loopengine 0.1.5 → 0.1.7
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/PROTOCOL.md +1 -1
- package/README.md +12 -0
- package/dist/bin/cli.d.ts +1 -1
- package/dist/bin/cli.js +5 -0
- package/dist/core/agent-config.d.ts +22 -3
- package/dist/core/discover-agents.js +14 -2
- package/dist/core/model-calls/gemini-model-call.d.ts +17 -0
- package/dist/core/model-calls/gemini-model-call.js +53 -0
- package/dist/core/model-calls/glm-model-call.d.ts +15 -0
- package/dist/core/model-calls/glm-model-call.js +46 -0
- package/dist/core/model-calls/kimi-model-call.d.ts +15 -0
- package/dist/core/model-calls/kimi-model-call.js +59 -0
- package/dist/core/run-agent.js +17 -3
- package/dist/index.d.ts +8 -0
- package/dist/index.js +4 -0
- package/dist/web/agent-file-admin.d.ts +7 -1
- package/dist/web/agent-file-admin.js +55 -0
- package/dist/web/agents-config-page.js +330 -13
- package/dist/web/global-config.d.ts +1 -1
- package/dist/web/global-config.js +5 -0
- package/dist/web/skillgarden-admin.d.ts +24 -0
- package/dist/web/skillgarden-admin.js +76 -0
- package/package.json +8 -2
package/PROTOCOL.md
CHANGED
|
@@ -190,7 +190,7 @@ loopengine run customer-service --json "order A-1001 arrived broken, can you ref
|
|
|
190
190
|
|
|
191
191
|
```
|
|
192
192
|
{"type":"session","sessionId":"5a83c6b4-..."}
|
|
193
|
-
{"type":"budget:check","action":"ok","usedTokens":2,"budgetTokens":
|
|
193
|
+
{"type":"budget:check","action":"ok","usedTokens":2,"budgetTokens":100000,"ratio":0.00002}
|
|
194
194
|
{"type":"tool:started","id":"call_1","tool":"issue_refund", ...}
|
|
195
195
|
{"type":"done","text":"Refunded $42.00..."}
|
|
196
196
|
```
|
package/README.md
CHANGED
|
@@ -466,6 +466,18 @@ you, using the matching API key from the environment:
|
|
|
466
466
|
| `'anthropic'` | `ANTHROPIC_API_KEY` | no — defaults to `claude-sonnet-5` |
|
|
467
467
|
| `'openai'` | `OPENAI_API_KEY` | yes |
|
|
468
468
|
| `'deepseek'` | `DEEPSEEK_API_KEY` | yes |
|
|
469
|
+
| `'kimi'` | `MOONSHOT_API_KEY` | yes |
|
|
470
|
+
| `'glm'` | `GLM_API_KEY` | yes |
|
|
471
|
+
| `'gemini'` | `GEMINI_API_KEY` | yes |
|
|
472
|
+
|
|
473
|
+
`'kimi'`'s env var is `MOONSHOT_API_KEY`, not `KIMI_API_KEY` — deliberate,
|
|
474
|
+
matching Moonshot AI's own docs (the API/company behind Kimi) rather than
|
|
475
|
+
this package's own provider name. `openai`/`deepseek`/`kimi`/`glm`/
|
|
476
|
+
`gemini` all reuse the same OpenAI-Chat-Completions-compatible request
|
|
477
|
+
translation, just pointed at each provider's own base URL — see
|
|
478
|
+
`core/model-calls/*.ts` for the provider-specific details (base URLs,
|
|
479
|
+
`max_completion_tokens` vs `max_tokens`, and Gemini's own "still in beta"
|
|
480
|
+
caveat on Google's compatibility layer).
|
|
469
481
|
|
|
470
482
|
For anything else (a custom SDK client, a canned/simulated model for
|
|
471
483
|
testing), export your own `createModelCall(): ModelCall` instead.
|
package/dist/bin/cli.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export declare class AgentNotFoundError extends Error {
|
|
|
7
7
|
}
|
|
8
8
|
export declare class AgentModelError extends Error {
|
|
9
9
|
}
|
|
10
|
-
type Provider = 'anthropic' | 'openai' | 'deepseek';
|
|
10
|
+
type Provider = 'anthropic' | 'openai' | 'deepseek' | 'kimi' | 'glm' | 'gemini';
|
|
11
11
|
export interface AgentTemplateOptions {
|
|
12
12
|
systemPrompt?: string;
|
|
13
13
|
model?: {
|
package/dist/bin/cli.js
CHANGED
|
@@ -26,6 +26,11 @@ const MODEL_ENV_VAR = {
|
|
|
26
26
|
anthropic: 'ANTHROPIC_API_KEY',
|
|
27
27
|
openai: 'OPENAI_API_KEY',
|
|
28
28
|
deepseek: 'DEEPSEEK_API_KEY',
|
|
29
|
+
// Moonshot AI's own env var name, not KIMI_API_KEY — see
|
|
30
|
+
// kimi-model-call.ts's own header comment for why.
|
|
31
|
+
kimi: 'MOONSHOT_API_KEY',
|
|
32
|
+
glm: 'GLM_API_KEY',
|
|
33
|
+
gemini: 'GEMINI_API_KEY',
|
|
29
34
|
};
|
|
30
35
|
// Escapes a value for safe interpolation into a single-quoted TS string
|
|
31
36
|
// literal in generated code — an operator-supplied systemPrompt (from
|
|
@@ -237,9 +237,10 @@ export type HttpNotifierConfig = {
|
|
|
237
237
|
* export your own `createModelCall` for anything this can't express (a
|
|
238
238
|
* canned/simulated `ModelCall` for a demo, a custom SDK client, a
|
|
239
239
|
* provider this doesn't list). `model` is required for `openai`/
|
|
240
|
-
* `deepseek` (no safe hardcoded default — a
|
|
241
|
-
* too often to bake one in) but optional for
|
|
242
|
-
* `'claude-sonnet-5'`), mirroring each factory's
|
|
240
|
+
* `deepseek`/`kimi`/`glm`/`gemini` (no safe hardcoded default — a
|
|
241
|
+
* flagship model name changes too often to bake one in) but optional for
|
|
242
|
+
* `anthropic` (defaults to `'claude-sonnet-5'`), mirroring each factory's
|
|
243
|
+
* own options exactly. */
|
|
243
244
|
export type AgentModelConfig = {
|
|
244
245
|
provider: 'anthropic';
|
|
245
246
|
model?: string;
|
|
@@ -256,6 +257,24 @@ export type AgentModelConfig = {
|
|
|
256
257
|
apiKey?: string;
|
|
257
258
|
maxTokens?: number;
|
|
258
259
|
baseURL?: string;
|
|
260
|
+
} | {
|
|
261
|
+
provider: 'kimi';
|
|
262
|
+
model: string;
|
|
263
|
+
apiKey?: string;
|
|
264
|
+
maxTokens?: number;
|
|
265
|
+
baseURL?: string;
|
|
266
|
+
} | {
|
|
267
|
+
provider: 'glm';
|
|
268
|
+
model: string;
|
|
269
|
+
apiKey?: string;
|
|
270
|
+
maxTokens?: number;
|
|
271
|
+
baseURL?: string;
|
|
272
|
+
} | {
|
|
273
|
+
provider: 'gemini';
|
|
274
|
+
model: string;
|
|
275
|
+
apiKey?: string;
|
|
276
|
+
maxTokens?: number;
|
|
277
|
+
baseURL?: string;
|
|
259
278
|
};
|
|
260
279
|
export interface ToolSchema {
|
|
261
280
|
name: string;
|
|
@@ -47,8 +47,20 @@ export async function synthesizeCreateModelCall(model) {
|
|
|
47
47
|
const { createOpenAIModelCall } = await import('./model-calls/openai-model-call.js');
|
|
48
48
|
return () => (cached ??= createOpenAIModelCall(model));
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
if (model.provider === 'deepseek') {
|
|
51
|
+
const { createDeepSeekModelCall } = await import('./model-calls/deepseek-model-call.js');
|
|
52
|
+
return () => (cached ??= createDeepSeekModelCall(model));
|
|
53
|
+
}
|
|
54
|
+
if (model.provider === 'kimi') {
|
|
55
|
+
const { createKimiModelCall } = await import('./model-calls/kimi-model-call.js');
|
|
56
|
+
return () => (cached ??= createKimiModelCall(model));
|
|
57
|
+
}
|
|
58
|
+
if (model.provider === 'glm') {
|
|
59
|
+
const { createGlmModelCall } = await import('./model-calls/glm-model-call.js');
|
|
60
|
+
return () => (cached ??= createGlmModelCall(model));
|
|
61
|
+
}
|
|
62
|
+
const { createGeminiModelCall } = await import('./model-calls/gemini-model-call.js');
|
|
63
|
+
return () => (cached ??= createGeminiModelCall(model));
|
|
52
64
|
}
|
|
53
65
|
/** Resolves one directory entry to an agent module's path, or undefined if
|
|
54
66
|
* this entry isn't one — a `.ts`/`.js` file directly (the common case), or
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import type { ModelCall } from '#core/run-agent.js';
|
|
3
|
+
export interface GeminiModelCallOptions {
|
|
4
|
+
/** Defaults to the GEMINI_API_KEY env var. */
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
/** No hardcoded default — same reasoning as OpenAIModelCallOptions.model:
|
|
7
|
+
* pass the one you actually want. */
|
|
8
|
+
model: string;
|
|
9
|
+
maxTokens?: number;
|
|
10
|
+
/** Defaults to Google's own OpenAI-compatible endpoint — override to
|
|
11
|
+
* point at a proxy/mirror. Trailing slash required (see this file's
|
|
12
|
+
* own header comment). */
|
|
13
|
+
baseURL?: string;
|
|
14
|
+
/** Inject a pre-configured client instead of building one from apiKey/baseURL — e.g. to pass a custom `fetch` in tests. */
|
|
15
|
+
client?: OpenAI;
|
|
16
|
+
}
|
|
17
|
+
export declare function createGeminiModelCall(options: GeminiModelCallOptions): ModelCall;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// A sixth real ModelCall implementation. Google's own OpenAI-compatible
|
|
2
|
+
// endpoint for Gemini (ai.google.dev/gemini-api/docs/openai) supports the
|
|
3
|
+
// same messages/tools/tool_choice request shape and tool_calls response
|
|
4
|
+
// shape as OpenAI's Chat Completions API, so this reuses
|
|
5
|
+
// openai-model-call.ts's translation verbatim via the real `openai` SDK
|
|
6
|
+
// client, same pattern deepseek/kimi/glm-model-call.ts already
|
|
7
|
+
// established — rather than a bespoke native @google/genai integration
|
|
8
|
+
// (Gemini's own SDK has a meaningfully different message-role/content-
|
|
9
|
+
// parts/function-calling shape).
|
|
10
|
+
//
|
|
11
|
+
// Caveat, from Google's own docs: "Support for the OpenAI libraries is
|
|
12
|
+
// still in beta while we extend feature support" — this is the one
|
|
13
|
+
// provider here riding a compatibility layer its own vendor calls beta,
|
|
14
|
+
// not a fully stable wire contract the way DeepSeek/Kimi/GLM's own docs
|
|
15
|
+
// present theirs. Uses `max_completion_tokens`, same as Kimi/GLM.
|
|
16
|
+
//
|
|
17
|
+
// The trailing slash on the base URL matters — Google's endpoint 404s
|
|
18
|
+
// without it (confirmed against their own docs' exact example URL).
|
|
19
|
+
import OpenAI from 'openai';
|
|
20
|
+
import { toMessageParams, toModelResponse } from './openai-model-call.js';
|
|
21
|
+
export function createGeminiModelCall(options) {
|
|
22
|
+
if (!options.client && !options.apiKey && !process.env.GEMINI_API_KEY) {
|
|
23
|
+
// Fails here, with a Gemini-specific message, rather than letting the
|
|
24
|
+
// OpenAI SDK's own constructor validation reject it — that error says
|
|
25
|
+
// "set OPENAI_API_KEY", which would send someone who never touched
|
|
26
|
+
// that variable chasing the wrong fix.
|
|
27
|
+
throw new Error('createGeminiModelCall: no apiKey given and GEMINI_API_KEY is not set.');
|
|
28
|
+
}
|
|
29
|
+
const client = options.client ??
|
|
30
|
+
new OpenAI({
|
|
31
|
+
// Explicitly null, not undefined, if neither is set — see
|
|
32
|
+
// kimi-model-call.ts's identical comment for why this matters even
|
|
33
|
+
// though the guard above already rules out reaching here with no
|
|
34
|
+
// key at all.
|
|
35
|
+
apiKey: options.apiKey ?? process.env.GEMINI_API_KEY ?? null,
|
|
36
|
+
baseURL: options.baseURL ?? 'https://generativelanguage.googleapis.com/v1beta/openai/',
|
|
37
|
+
});
|
|
38
|
+
const maxTokens = options.maxTokens ?? 4096;
|
|
39
|
+
return async (messages, system, tools) => {
|
|
40
|
+
const response = await client.chat.completions.create({
|
|
41
|
+
model: options.model,
|
|
42
|
+
max_completion_tokens: maxTokens,
|
|
43
|
+
messages: [{ role: 'system', content: system }, ...messages.flatMap(toMessageParams)],
|
|
44
|
+
tools: tools.length
|
|
45
|
+
? tools.map((t) => ({
|
|
46
|
+
type: 'function',
|
|
47
|
+
function: { name: t.name, description: t.description, parameters: t.input_schema },
|
|
48
|
+
}))
|
|
49
|
+
: undefined,
|
|
50
|
+
});
|
|
51
|
+
return toModelResponse(response.choices[0]);
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import type { ModelCall } from '#core/run-agent.js';
|
|
3
|
+
export interface GlmModelCallOptions {
|
|
4
|
+
/** Defaults to the GLM_API_KEY env var. */
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
/** No hardcoded default — same reasoning as OpenAIModelCallOptions.model:
|
|
7
|
+
* pass the one you actually want. */
|
|
8
|
+
model: string;
|
|
9
|
+
maxTokens?: number;
|
|
10
|
+
/** Defaults to Zhipu's own API — override to point at a proxy/mirror. */
|
|
11
|
+
baseURL?: string;
|
|
12
|
+
/** Inject a pre-configured client instead of building one from apiKey/baseURL — e.g. to pass a custom `fetch` in tests. */
|
|
13
|
+
client?: OpenAI;
|
|
14
|
+
}
|
|
15
|
+
export declare function createGlmModelCall(options: GlmModelCallOptions): ModelCall;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// A fifth real ModelCall implementation. GLM (Zhipu AI / Z.ai)'s Chat
|
|
2
|
+
// Completions API is OpenAI-compatible — confirmed against Zhipu's own
|
|
3
|
+
// docs (docs.bigmodel.cn/cn/guide/develop/openai/introduction): same
|
|
4
|
+
// messages/tools/tool_choice request shape, same tool_calls response
|
|
5
|
+
// shape — so this reuses openai-model-call.ts's message/response
|
|
6
|
+
// translation verbatim via the real `openai` SDK client, just pointed at
|
|
7
|
+
// Zhipu's base URL, rather than duplicating that logic. Same pattern
|
|
8
|
+
// deepseek-model-call.ts/kimi-model-call.ts already established.
|
|
9
|
+
//
|
|
10
|
+
// Uses the newer `max_completion_tokens` field, same as Kimi — no
|
|
11
|
+
// DeepSeek-style documented exception for GLM.
|
|
12
|
+
import OpenAI from 'openai';
|
|
13
|
+
import { toMessageParams, toModelResponse } from './openai-model-call.js';
|
|
14
|
+
export function createGlmModelCall(options) {
|
|
15
|
+
if (!options.client && !options.apiKey && !process.env.GLM_API_KEY) {
|
|
16
|
+
// Fails here, with a GLM-specific message, rather than letting the
|
|
17
|
+
// OpenAI SDK's own constructor validation reject it — that error says
|
|
18
|
+
// "set OPENAI_API_KEY", which would send someone who never touched
|
|
19
|
+
// that variable chasing the wrong fix.
|
|
20
|
+
throw new Error('createGlmModelCall: no apiKey given and GLM_API_KEY is not set.');
|
|
21
|
+
}
|
|
22
|
+
const client = options.client ??
|
|
23
|
+
new OpenAI({
|
|
24
|
+
// Explicitly null, not undefined, if neither is set — see
|
|
25
|
+
// kimi-model-call.ts's identical comment for why this matters even
|
|
26
|
+
// though the guard above already rules out reaching here with no
|
|
27
|
+
// key at all.
|
|
28
|
+
apiKey: options.apiKey ?? process.env.GLM_API_KEY ?? null,
|
|
29
|
+
baseURL: options.baseURL ?? 'https://open.bigmodel.cn/api/paas/v4/',
|
|
30
|
+
});
|
|
31
|
+
const maxTokens = options.maxTokens ?? 4096;
|
|
32
|
+
return async (messages, system, tools) => {
|
|
33
|
+
const response = await client.chat.completions.create({
|
|
34
|
+
model: options.model,
|
|
35
|
+
max_completion_tokens: maxTokens,
|
|
36
|
+
messages: [{ role: 'system', content: system }, ...messages.flatMap(toMessageParams)],
|
|
37
|
+
tools: tools.length
|
|
38
|
+
? tools.map((t) => ({
|
|
39
|
+
type: 'function',
|
|
40
|
+
function: { name: t.name, description: t.description, parameters: t.input_schema },
|
|
41
|
+
}))
|
|
42
|
+
: undefined,
|
|
43
|
+
});
|
|
44
|
+
return toModelResponse(response.choices[0]);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import OpenAI from 'openai';
|
|
2
|
+
import type { ModelCall } from '#core/run-agent.js';
|
|
3
|
+
export interface KimiModelCallOptions {
|
|
4
|
+
/** Defaults to the MOONSHOT_API_KEY env var. */
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
/** No hardcoded default — same reasoning as OpenAIModelCallOptions.model:
|
|
7
|
+
* pass the one you actually want. */
|
|
8
|
+
model: string;
|
|
9
|
+
maxTokens?: number;
|
|
10
|
+
/** Defaults to Moonshot's own API — override to point at a proxy/mirror. */
|
|
11
|
+
baseURL?: string;
|
|
12
|
+
/** Inject a pre-configured client instead of building one from apiKey/baseURL — e.g. to pass a custom `fetch` in tests. */
|
|
13
|
+
client?: OpenAI;
|
|
14
|
+
}
|
|
15
|
+
export declare function createKimiModelCall(options: KimiModelCallOptions): ModelCall;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// A fourth real ModelCall implementation. Kimi (Moonshot AI)'s Chat
|
|
2
|
+
// Completions API is wire-compatible with OpenAI's — confirmed against
|
|
3
|
+
// Moonshot's own docs (platform.kimi.ai/docs/api/chat): same messages/
|
|
4
|
+
// tools/tool_choice request shape, same tool_calls response shape — so
|
|
5
|
+
// this reuses openai-model-call.ts's message/response translation
|
|
6
|
+
// verbatim via the real `openai` SDK client, just pointed at Moonshot's
|
|
7
|
+
// base URL, rather than duplicating that logic. Same pattern
|
|
8
|
+
// deepseek-model-call.ts already established for a third provider.
|
|
9
|
+
//
|
|
10
|
+
// Unlike DeepSeek, Moonshot's docs use the newer `max_completion_tokens`
|
|
11
|
+
// field (matching the current OpenAI SDK) rather than the deprecated
|
|
12
|
+
// `max_tokens` DeepSeek's own docs still list — decided at the request-
|
|
13
|
+
// building call site below, since toMessageParams/toModelResponse don't
|
|
14
|
+
// touch that field either way.
|
|
15
|
+
//
|
|
16
|
+
// Env var is MOONSHOT_API_KEY, not KIMI_API_KEY, despite every other
|
|
17
|
+
// provider here matching its own literal exactly — deliberate: this
|
|
18
|
+
// matches the underlying API/company's own docs and what anyone
|
|
19
|
+
// copy-pasting from Moonshot's own examples already has set, not this
|
|
20
|
+
// package's own provider name.
|
|
21
|
+
import OpenAI from 'openai';
|
|
22
|
+
import { toMessageParams, toModelResponse } from './openai-model-call.js';
|
|
23
|
+
export function createKimiModelCall(options) {
|
|
24
|
+
if (!options.client && !options.apiKey && !process.env.MOONSHOT_API_KEY) {
|
|
25
|
+
// Fails here, with a Kimi-specific message, rather than letting the
|
|
26
|
+
// OpenAI SDK's own constructor validation reject it — that error says
|
|
27
|
+
// "set OPENAI_API_KEY", which would send someone who never touched
|
|
28
|
+
// that variable chasing the wrong fix.
|
|
29
|
+
throw new Error('createKimiModelCall: no apiKey given and MOONSHOT_API_KEY is not set.');
|
|
30
|
+
}
|
|
31
|
+
const client = options.client ??
|
|
32
|
+
new OpenAI({
|
|
33
|
+
// Explicitly null, not undefined, if neither is set — the OpenAI
|
|
34
|
+
// SDK's own constructor falls back to reading OPENAI_API_KEY itself
|
|
35
|
+
// when apiKey is undefined (its normal, correct behavior for
|
|
36
|
+
// createOpenAIModelCall), which here would silently authenticate
|
|
37
|
+
// against Moonshot with an OpenAI key if one happens to be set. The
|
|
38
|
+
// guard above already rules out reaching here with no key at all,
|
|
39
|
+
// but null (not undefined) is kept regardless so this line is
|
|
40
|
+
// correct on its own, not just because of the guard before it.
|
|
41
|
+
apiKey: options.apiKey ?? process.env.MOONSHOT_API_KEY ?? null,
|
|
42
|
+
baseURL: options.baseURL ?? 'https://api.moonshot.ai/v1',
|
|
43
|
+
});
|
|
44
|
+
const maxTokens = options.maxTokens ?? 4096;
|
|
45
|
+
return async (messages, system, tools) => {
|
|
46
|
+
const response = await client.chat.completions.create({
|
|
47
|
+
model: options.model,
|
|
48
|
+
max_completion_tokens: maxTokens,
|
|
49
|
+
messages: [{ role: 'system', content: system }, ...messages.flatMap(toMessageParams)],
|
|
50
|
+
tools: tools.length
|
|
51
|
+
? tools.map((t) => ({
|
|
52
|
+
type: 'function',
|
|
53
|
+
function: { name: t.name, description: t.description, parameters: t.input_schema },
|
|
54
|
+
}))
|
|
55
|
+
: undefined,
|
|
56
|
+
});
|
|
57
|
+
return toModelResponse(response.choices[0]);
|
|
58
|
+
};
|
|
59
|
+
}
|
package/dist/core/run-agent.js
CHANGED
|
@@ -310,7 +310,12 @@ async function buildTurnContext(config, modelCall, options) {
|
|
|
310
310
|
// that doesn't exist (a flat-file agent with no skills at all)
|
|
311
311
|
// harmless: an empty index, not an error.
|
|
312
312
|
const skillsDirs = config.skillsDirs ?? [`agents/${config.name}/skills`];
|
|
313
|
-
|
|
313
|
+
// 2000, not an arbitrary round number: skillgarden's own default is
|
|
314
|
+
// ~1% of a real agent context window (~100k-200k tokens for the
|
|
315
|
+
// providers this repo wires up) — see its own SkillGardenOptions doc
|
|
316
|
+
// comment. loopengine used to default lower (200), which left barely
|
|
317
|
+
// enough room for a handful of skills before truncating the index.
|
|
318
|
+
const skillGarden = new SkillGarden({ dirs: [...skillsDirs, systemSkillsDir], indexBudgetTokens: config.skillIndexBudgetTokens ?? 2000 });
|
|
314
319
|
const skillIndex = skillGarden.buildIndex().included;
|
|
315
320
|
// Also the tail-preservation window recover() below relies on — kept as
|
|
316
321
|
// one named constant so the two can never drift out of sync.
|
|
@@ -321,8 +326,17 @@ async function buildTurnContext(config, modelCall, options) {
|
|
|
321
326
|
// budgetTokens/softThreshold so a nudge firing and compaction's own
|
|
322
327
|
// recovery target agree — see budget.ts's own BudgetTrackerOptions doc
|
|
323
328
|
// comment.
|
|
324
|
-
|
|
325
|
-
|
|
329
|
+
// 100000, not 8000: the providers this repo wires up (Claude, GPT-4o,
|
|
330
|
+
// DeepSeek) all give a real conversation 128k-200k tokens of window,
|
|
331
|
+
// and 8000 was small enough that a handful of tool calls (each with a
|
|
332
|
+
// full JSON schema + result) routinely tripped compaction on an
|
|
333
|
+
// otherwise ordinary turn. 100000 leaves headroom under even the
|
|
334
|
+
// smallest of those windows for the system prompt, tool schemas, and
|
|
335
|
+
// the model's own response, while softThreshold's default 0.7 (a
|
|
336
|
+
// 70000-token nudge point) and hardThreshold's 0.92 still leave real
|
|
337
|
+
// margin before hitting it.
|
|
338
|
+
const budgetTracker = new BudgetTracker({ budgetTokens: config.contextBudgetTokens ?? 100000 });
|
|
339
|
+
const compactor = new Compactor({ budgetTokens: config.contextBudgetTokens ?? 100000, softThreshold: budgetTracker.softThreshold, tailMessages });
|
|
326
340
|
const rules = loadRules(config);
|
|
327
341
|
// AgentConfig.httpNotifier only ever stands in for the http channel —
|
|
328
342
|
// see its own doc comment for why cli/http_stream never consult it at
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,12 @@ export { createOpenAIModelCall } from './core/model-calls/openai-model-call.js';
|
|
|
14
14
|
export type { OpenAIModelCallOptions } from './core/model-calls/openai-model-call.js';
|
|
15
15
|
export { createDeepSeekModelCall } from './core/model-calls/deepseek-model-call.js';
|
|
16
16
|
export type { DeepSeekModelCallOptions } from './core/model-calls/deepseek-model-call.js';
|
|
17
|
+
export { createKimiModelCall } from './core/model-calls/kimi-model-call.js';
|
|
18
|
+
export type { KimiModelCallOptions } from './core/model-calls/kimi-model-call.js';
|
|
19
|
+
export { createGlmModelCall } from './core/model-calls/glm-model-call.js';
|
|
20
|
+
export type { GlmModelCallOptions } from './core/model-calls/glm-model-call.js';
|
|
21
|
+
export { createGeminiModelCall } from './core/model-calls/gemini-model-call.js';
|
|
22
|
+
export type { GeminiModelCallOptions } from './core/model-calls/gemini-model-call.js';
|
|
17
23
|
export { discoverAgents, loadAgentModule, synthesizeCreateModelCall } from './core/discover-agents.js';
|
|
18
24
|
export type { AgentModule } from './core/discover-agents.js';
|
|
19
25
|
export { agentAsTool } from './core/agent-as-tool.js';
|
|
@@ -35,6 +41,8 @@ export { globalConfigPageHtml } from './web/global-config-page.js';
|
|
|
35
41
|
export { playgroundHtml } from './web/playground.js';
|
|
36
42
|
export { readSkill, writeSkill, deleteSkill, SkillInvalidIdError, SkillNotFoundError } from './web/skills-admin.js';
|
|
37
43
|
export type { SkillContent } from './web/skills-admin.js';
|
|
44
|
+
export { listSkillgardenCatalog, readSkillgardenCatalogEntry, addSkillgardenSkillToAgent, SkillgardenUnavailableError } from './web/skillgarden-admin.js';
|
|
45
|
+
export type { SkillgardenCatalogEntry, SkillgardenCatalogDetail } from './web/skillgarden-admin.js';
|
|
38
46
|
export { readActauthConfig, addActauthRule, updateActauthRule, removeActauthRule, setDefaultDecision, ActauthRuleExistsError, ActauthRuleNotFoundError, } from './web/actauth-admin.js';
|
|
39
47
|
export type { ActauthRuleInput, ActauthConfigView } from './web/actauth-admin.js';
|
|
40
48
|
export { describeModelProviders, describeGateways } from './web/global-config.js';
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,9 @@ export { VectorIndex, embed, cosineSimilarity } from './core/vector-index.js';
|
|
|
16
16
|
export { createAnthropicModelCall } from './core/model-calls/anthropic-model-call.js';
|
|
17
17
|
export { createOpenAIModelCall } from './core/model-calls/openai-model-call.js';
|
|
18
18
|
export { createDeepSeekModelCall } from './core/model-calls/deepseek-model-call.js';
|
|
19
|
+
export { createKimiModelCall } from './core/model-calls/kimi-model-call.js';
|
|
20
|
+
export { createGlmModelCall } from './core/model-calls/glm-model-call.js';
|
|
21
|
+
export { createGeminiModelCall } from './core/model-calls/gemini-model-call.js';
|
|
19
22
|
export { discoverAgents, loadAgentModule, synthesizeCreateModelCall } from './core/discover-agents.js';
|
|
20
23
|
export { agentAsTool } from './core/agent-as-tool.js';
|
|
21
24
|
// Everything below backs the Admin UI (agents-config/list/global-config
|
|
@@ -40,6 +43,7 @@ export { agentsListPageHtml } from './web/agents-list-page.js';
|
|
|
40
43
|
export { globalConfigPageHtml } from './web/global-config-page.js';
|
|
41
44
|
export { playgroundHtml } from './web/playground.js';
|
|
42
45
|
export { readSkill, writeSkill, deleteSkill, SkillInvalidIdError, SkillNotFoundError } from './web/skills-admin.js';
|
|
46
|
+
export { listSkillgardenCatalog, readSkillgardenCatalogEntry, addSkillgardenSkillToAgent, SkillgardenUnavailableError } from './web/skillgarden-admin.js';
|
|
43
47
|
export { readActauthConfig, addActauthRule, updateActauthRule, removeActauthRule, setDefaultDecision, ActauthRuleExistsError, ActauthRuleNotFoundError, } from './web/actauth-admin.js';
|
|
44
48
|
export { describeModelProviders, describeGateways } from './web/global-config.js';
|
|
45
49
|
export { createTrackedApprover, listApprovals, decideApproval, findApproval } from './web/web-approver.js';
|
|
@@ -2,13 +2,16 @@ export declare class AgentEditNotSupportedError extends Error {
|
|
|
2
2
|
}
|
|
3
3
|
export declare class AgentFileNotFoundError extends Error {
|
|
4
4
|
}
|
|
5
|
-
type Provider = 'anthropic' | 'openai' | 'deepseek';
|
|
5
|
+
type Provider = 'anthropic' | 'openai' | 'deepseek' | 'kimi' | 'glm' | 'gemini';
|
|
6
6
|
export interface AgentEditableFields {
|
|
7
7
|
systemPrompt?: string;
|
|
8
8
|
model?: {
|
|
9
9
|
provider: Provider;
|
|
10
10
|
model?: string;
|
|
11
11
|
};
|
|
12
|
+
maxTurns?: number;
|
|
13
|
+
contextBudgetTokens?: number;
|
|
14
|
+
skillIndexBudgetTokens?: number;
|
|
12
15
|
}
|
|
13
16
|
export interface AgentEditResult {
|
|
14
17
|
systemPrompt?: string;
|
|
@@ -16,6 +19,9 @@ export interface AgentEditResult {
|
|
|
16
19
|
provider: Provider;
|
|
17
20
|
model: string;
|
|
18
21
|
};
|
|
22
|
+
maxTurns?: number;
|
|
23
|
+
contextBudgetTokens?: number;
|
|
24
|
+
skillIndexBudgetTokens?: number;
|
|
19
25
|
}
|
|
20
26
|
/** Surgically rewrites just the fields given in `fields` — see this
|
|
21
27
|
* file's own header comment for why, and for the "refuse rather than
|
|
@@ -45,6 +45,11 @@ const MODEL_ENV_VAR = {
|
|
|
45
45
|
anthropic: 'ANTHROPIC_API_KEY',
|
|
46
46
|
openai: 'OPENAI_API_KEY',
|
|
47
47
|
deepseek: 'DEEPSEEK_API_KEY',
|
|
48
|
+
// Moonshot AI's own env var name, not KIMI_API_KEY — see
|
|
49
|
+
// core/model-calls/kimi-model-call.ts's own header comment for why.
|
|
50
|
+
kimi: 'MOONSHOT_API_KEY',
|
|
51
|
+
glm: 'GLM_API_KEY',
|
|
52
|
+
gemini: 'GEMINI_API_KEY',
|
|
48
53
|
};
|
|
49
54
|
// The exact shape agentIndexTemplate (bin/cli.ts) always generates for the
|
|
50
55
|
// model field's trailing comment — used to recognize a comment this
|
|
@@ -89,6 +94,31 @@ function findConfigObjectLiteral(sourceFile) {
|
|
|
89
94
|
function findProperty(obj, name) {
|
|
90
95
|
return obj.properties.find((p) => p.name !== undefined && ts.isIdentifier(p.name) && p.name.text === name);
|
|
91
96
|
}
|
|
97
|
+
// maxTurns/contextBudgetTokens/skillIndexBudgetTokens are the only three
|
|
98
|
+
// AgentConfig fields this file edits that are *usually absent* — every
|
|
99
|
+
// agent in this repo, and create-loopengine's own scaffold template,
|
|
100
|
+
// leaves all three unset and rides run-agent.ts's own defaults (25 /
|
|
101
|
+
// 100000 / 2000), unlike systemPrompt/model which agentIndexTemplate always
|
|
102
|
+
// writes out explicitly. So "the property isn't in this file" can't mean
|
|
103
|
+
// refuse here the way it does for a genuinely unusual systemPrompt/model
|
|
104
|
+
// shape — that would make every freshly scaffolded agent's limits
|
|
105
|
+
// permanently uneditable through this API. Missing properties are
|
|
106
|
+
// collected into `toInsert` instead and spliced in as new lines just
|
|
107
|
+
// before the object literal's closing brace (see editAgentFile's own use
|
|
108
|
+
// of this); an *existing* property whose value isn't a plain number
|
|
109
|
+
// literal is still refused, same "don't guess" rule as everything else
|
|
110
|
+
// in this file.
|
|
111
|
+
function upsertNumericProperty(configObj, sourceFile, edits, toInsert, name, value) {
|
|
112
|
+
const prop = findProperty(configObj, name);
|
|
113
|
+
if (!prop) {
|
|
114
|
+
toInsert.push(`${name}: ${value}`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (!ts.isPropertyAssignment(prop) || !ts.isNumericLiteral(prop.initializer)) {
|
|
118
|
+
throw new AgentEditNotSupportedError(`${name} in this file is not a plain number literal — edit it directly.`);
|
|
119
|
+
}
|
|
120
|
+
edits.push({ start: prop.initializer.getStart(sourceFile), end: prop.initializer.getEnd(), text: String(value) });
|
|
121
|
+
}
|
|
92
122
|
/** Surgically rewrites just the fields given in `fields` — see this
|
|
93
123
|
* file's own header comment for why, and for the "refuse rather than
|
|
94
124
|
* guess" rule this follows for anything that isn't a plain, recognizable
|
|
@@ -150,6 +180,31 @@ export function editAgentFile(agentName, fields) {
|
|
|
150
180
|
edits.push({ start: comment.pos, end: comment.end, text: `// reads ${MODEL_ENV_VAR[provider]}` });
|
|
151
181
|
}
|
|
152
182
|
}
|
|
183
|
+
const toInsert = [];
|
|
184
|
+
for (const name of ['maxTurns', 'contextBudgetTokens', 'skillIndexBudgetTokens']) {
|
|
185
|
+
const value = fields[name];
|
|
186
|
+
if (value === undefined)
|
|
187
|
+
continue;
|
|
188
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
189
|
+
throw new AgentEditNotSupportedError(`${name} must be a positive integer.`);
|
|
190
|
+
}
|
|
191
|
+
upsertNumericProperty(configObj, sourceFile, edits, toInsert, name, value);
|
|
192
|
+
result[name] = value;
|
|
193
|
+
}
|
|
194
|
+
if (toInsert.length) {
|
|
195
|
+
// Insert right before the closing "}", not right after the last
|
|
196
|
+
// property node — a hand-written trailing line comment there (like
|
|
197
|
+
// model's own "// reads ANTHROPIC_API_KEY" above) would otherwise end
|
|
198
|
+
// up with new properties spliced into the middle of it. The object's
|
|
199
|
+
// own pre-existing newline-before-"}" becomes the separator before
|
|
200
|
+
// the first inserted line for free; only an object with *no*
|
|
201
|
+
// properties at all needs its own leading newline.
|
|
202
|
+
const closeBracePos = configObj.getEnd() - 1;
|
|
203
|
+
const hasProps = configObj.properties.length > 0;
|
|
204
|
+
const needsLeadingComma = hasProps && !configObj.properties.hasTrailingComma;
|
|
205
|
+
const text = (hasProps ? '' : '\n') + (needsLeadingComma ? ',' : '') + toInsert.map((p) => ` ${p},\n`).join('');
|
|
206
|
+
edits.push({ start: closeBracePos, end: closeBracePos, text });
|
|
207
|
+
}
|
|
153
208
|
if (!edits.length)
|
|
154
209
|
return result;
|
|
155
210
|
// Apply from the end of the file backward — each edit's start/end was
|
|
@@ -131,12 +131,12 @@ export const agentsConfigPageHtml = `<!doctype html>
|
|
|
131
131
|
letter-spacing: normal;
|
|
132
132
|
padding: 2px 8px;
|
|
133
133
|
}
|
|
134
|
-
.
|
|
134
|
+
.admin-subsection {
|
|
135
135
|
margin-top: 14px;
|
|
136
136
|
padding-top: 14px;
|
|
137
137
|
border-top: 1px dashed light-dark(#eee, #2a2a2e);
|
|
138
138
|
}
|
|
139
|
-
.
|
|
139
|
+
.admin-subsection h4 {
|
|
140
140
|
font-size: 12px;
|
|
141
141
|
text-transform: uppercase;
|
|
142
142
|
letter-spacing: 0.04em;
|
|
@@ -309,6 +309,84 @@ export const agentsConfigPageHtml = `<!doctype html>
|
|
|
309
309
|
stretches the checkbox to the full row width, which visually
|
|
310
310
|
overlaps the description text under it instead of sitting beside it. */
|
|
311
311
|
form.add-source input[type="checkbox"] { width: auto; flex-shrink: 0; margin-top: 2px; }
|
|
312
|
+
/* Same column-stacking issue as .tool-picker-item above: .http-tool-
|
|
313
|
+
inline-label (the "required" / "send as JSON body" checkboxes in the
|
|
314
|
+
HTTP tool builder) is a <label> too, so "form.add-source label" wins
|
|
315
|
+
on specificity alone and stacks the checkbox above its own text
|
|
316
|
+
instead of beside it — this rule needs to match "label" too to win. */
|
|
317
|
+
form.add-source label.http-tool-inline-label {
|
|
318
|
+
display: flex;
|
|
319
|
+
flex-direction: row;
|
|
320
|
+
align-items: center;
|
|
321
|
+
gap: 6px;
|
|
322
|
+
}
|
|
323
|
+
/* These three are plain <div>s of sibling inputs/selects/buttons, not
|
|
324
|
+
<label>s, so none of the rules above touch them — without an explicit
|
|
325
|
+
row layout they just wrap at the browser's default inline spacing,
|
|
326
|
+
which reads as everything jammed together with no breathing room.
|
|
327
|
+
form.add-source itself caps at max-width: 480px (see above), which
|
|
328
|
+
isn't wide enough to hold a field row's 5 items (name/type/
|
|
329
|
+
description/required/delete) on one line at a readable width, so
|
|
330
|
+
this wraps rather than forcing a single cramped line — each item
|
|
331
|
+
gets a min-width so text inputs wrap onto their own line instead of
|
|
332
|
+
shrinking to an unreadable sliver. */
|
|
333
|
+
.http-tool-field-row, .http-tool-header-row, .http-tool-method-url {
|
|
334
|
+
display: flex;
|
|
335
|
+
flex-wrap: wrap;
|
|
336
|
+
align-items: center;
|
|
337
|
+
gap: 8px;
|
|
338
|
+
margin-bottom: 8px;
|
|
339
|
+
}
|
|
340
|
+
/* Same "form.add-source input { width: 100% }" override as the checkbox
|
|
341
|
+
fix above — each field here sits beside its siblings in a row, not
|
|
342
|
+
stacked full-width above them. */
|
|
343
|
+
.http-tool-field-row input[type="text"],
|
|
344
|
+
.http-tool-field-row select,
|
|
345
|
+
.http-tool-header-row input[type="text"],
|
|
346
|
+
.http-tool-method-url input[type="text"],
|
|
347
|
+
.http-tool-method-url select {
|
|
348
|
+
width: auto;
|
|
349
|
+
}
|
|
350
|
+
.http-tool-field-row .http-tool-field-name { flex: 1 1 100px; min-width: 100px; }
|
|
351
|
+
.http-tool-field-row .http-tool-field-type { flex: 0 0 auto; }
|
|
352
|
+
.http-tool-field-row .http-tool-field-description { flex: 2 1 140px; min-width: 140px; }
|
|
353
|
+
.http-tool-field-row .http-tool-inline-label { flex: 0 0 auto; white-space: nowrap; }
|
|
354
|
+
.http-tool-header-row .http-tool-header-key { flex: 1 1 100px; min-width: 100px; }
|
|
355
|
+
.http-tool-header-row .http-tool-header-value { flex: 2 1 140px; min-width: 140px; }
|
|
356
|
+
.http-tool-method-url select { flex: 0 0 auto; }
|
|
357
|
+
.http-tool-method-url input[type="text"] { flex: 1 1 160px; min-width: 160px; }
|
|
358
|
+
.http-tool-field-row .http-tool-remove-row,
|
|
359
|
+
.http-tool-header-row .http-tool-remove-row {
|
|
360
|
+
flex: 0 0 auto;
|
|
361
|
+
margin-left: auto;
|
|
362
|
+
}
|
|
363
|
+
/* Taller than .hint-btn's own 2px 8px default (shared with the Gateway
|
|
364
|
+
Tools "Refresh" button, which sits inline in an <h3> and shouldn't
|
|
365
|
+
grow) — these two sit on their own line below the field/header rows,
|
|
366
|
+
so there's no header-row height to match. */
|
|
367
|
+
#httpToolAddFieldBtn, #httpToolAddHeaderBtn {
|
|
368
|
+
padding: 6px 12px;
|
|
369
|
+
}
|
|
370
|
+
.skillgarden-category-badge {
|
|
371
|
+
display: inline-block;
|
|
372
|
+
font-family: ui-sans-serif, system-ui, sans-serif;
|
|
373
|
+
font-size: 10px;
|
|
374
|
+
font-weight: 600;
|
|
375
|
+
text-transform: uppercase;
|
|
376
|
+
letter-spacing: 0.03em;
|
|
377
|
+
padding: 1px 7px;
|
|
378
|
+
border-radius: 999px;
|
|
379
|
+
background: light-dark(#eef1ec, #1e2519);
|
|
380
|
+
color: light-dark(#3f6b4a, #7fbf8f);
|
|
381
|
+
margin-right: 6px;
|
|
382
|
+
}
|
|
383
|
+
#skillgardenFilterInput { margin-bottom: 8px; }
|
|
384
|
+
.skillgarden-catalog-body {
|
|
385
|
+
margin-top: 8px;
|
|
386
|
+
padding-top: 8px;
|
|
387
|
+
border-top: 1px solid light-dark(#eee, #2a2a2e);
|
|
388
|
+
font-size: 13px;
|
|
389
|
+
}
|
|
312
390
|
</style>
|
|
313
391
|
</head>
|
|
314
392
|
<body>
|
|
@@ -575,7 +653,7 @@ export const agentsConfigPageHtml = `<!doctype html>
|
|
|
575
653
|
'<form class="add-source" id="modelForm" style="display:none">' +
|
|
576
654
|
'<label>Provider' +
|
|
577
655
|
'<select name="provider">' +
|
|
578
|
-
['anthropic', 'openai', 'deepseek'].map(function (p) {
|
|
656
|
+
['anthropic', 'openai', 'deepseek', 'kimi', 'glm', 'gemini'].map(function (p) {
|
|
579
657
|
return '<option value="' + p + '"' + (p === cfg.model.provider ? ' selected' : '') + '>' + p + '</option>';
|
|
580
658
|
}).join('') +
|
|
581
659
|
'</select>' +
|
|
@@ -599,15 +677,31 @@ export const agentsConfigPageHtml = `<!doctype html>
|
|
|
599
677
|
'<dt>tenantFor</dt><dd>' + badge(cfg.tenantFor.indexOf('custom') === 0 ? 'custom' : 'default') + ' <span class="muted">' + escapeHtml(cfg.tenantFor) + '</span></dd>' +
|
|
600
678
|
'<dt>isSafeTool</dt><dd>' + badge(cfg.isSafeTool === 'custom' ? 'custom' : 'default') + ' <span class="muted">' + escapeHtml(cfg.isSafeTool) + '</span></dd>' +
|
|
601
679
|
'</dl></section>' +
|
|
602
|
-
'<section><h3>Limits & budgets
|
|
603
|
-
'<
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
'</dl
|
|
680
|
+
'<section><h3>Limits & budgets <button type="button" class="edit-btn" id="editLimitsBtn">Edit</button></h3>' +
|
|
681
|
+
'<dl class="kv" id="limitsDisplay">' +
|
|
682
|
+
'<dt>maxTurns</dt><dd>' + escapeHtml(cfg.maxTurns) + '</dd>' +
|
|
683
|
+
'<dt>contextBudgetTokens</dt><dd>' + escapeHtml(cfg.contextBudgetTokens) + '</dd>' +
|
|
684
|
+
'<dt>skillIndexBudgetTokens</dt><dd>' + escapeHtml(cfg.skillIndexBudgetTokens) + '</dd>' +
|
|
685
|
+
'</dl>' +
|
|
686
|
+
'<form class="add-source" id="limitsForm" style="display:none">' +
|
|
687
|
+
'<label>Max turns <span class="hint">(hard cap on model calls in one turn)</span>' +
|
|
688
|
+
'<input type="number" name="maxTurns" min="1" step="1" value="' + escapeHtml(cfg.maxTurns) + '">' +
|
|
689
|
+
'</label>' +
|
|
690
|
+
'<label>Context budget <span class="hint">(tokens — triggers compaction of older messages once exceeded)</span>' +
|
|
691
|
+
'<input type="number" name="contextBudgetTokens" min="1" step="1" value="' + escapeHtml(cfg.contextBudgetTokens) + '">' +
|
|
692
|
+
'</label>' +
|
|
693
|
+
'<label>Skill index budget <span class="hint">(tokens — caps the always-loaded skill name/description index)</span>' +
|
|
694
|
+
'<input type="number" name="skillIndexBudgetTokens" min="1" step="1" value="' + escapeHtml(cfg.skillIndexBudgetTokens) + '">' +
|
|
695
|
+
'</label>' +
|
|
696
|
+
'<button type="submit">Save</button>' +
|
|
697
|
+
'<button type="button" id="cancelLimitsBtn">Cancel</button>' +
|
|
698
|
+
'<div class="error" id="limitsError"></div>' +
|
|
699
|
+
'</form>' +
|
|
700
|
+
'<dl class="kv"><dt>skillsDirs</dt><dd>' + escapeHtml(cfg.skillsDirs.join(', ') || '(none)') + '</dd></dl>' +
|
|
701
|
+
'</section>';
|
|
608
702
|
}
|
|
609
703
|
|
|
610
|
-
// Wires the System prompt / Model Edit buttons rendered by
|
|
704
|
+
// Wires the System prompt / Model / Limits & budgets Edit buttons rendered by
|
|
611
705
|
// renderOverviewHtml above — called every time that HTML gets (re)set
|
|
612
706
|
// (renderDetail, refreshOverviewPanel, refreshSkillsDependentPanels),
|
|
613
707
|
// same pattern every other tab's own wireXHandlers already follows.
|
|
@@ -708,6 +802,53 @@ export const agentsConfigPageHtml = `<!doctype html>
|
|
|
708
802
|
});
|
|
709
803
|
});
|
|
710
804
|
}
|
|
805
|
+
|
|
806
|
+
var editLimitsBtn = overviewPanel.querySelector('#editLimitsBtn');
|
|
807
|
+
if (editLimitsBtn) {
|
|
808
|
+
var limitsDisplay = overviewPanel.querySelector('#limitsDisplay');
|
|
809
|
+
var limitsForm = overviewPanel.querySelector('#limitsForm');
|
|
810
|
+
var limitsError = overviewPanel.querySelector('#limitsError');
|
|
811
|
+
|
|
812
|
+
editLimitsBtn.addEventListener('click', function () {
|
|
813
|
+
limitsDisplay.style.display = 'none';
|
|
814
|
+
editLimitsBtn.style.display = 'none';
|
|
815
|
+
limitsForm.style.display = '';
|
|
816
|
+
});
|
|
817
|
+
overviewPanel.querySelector('#cancelLimitsBtn').addEventListener('click', function () {
|
|
818
|
+
limitsForm.style.display = 'none';
|
|
819
|
+
limitsDisplay.style.display = '';
|
|
820
|
+
editLimitsBtn.style.display = '';
|
|
821
|
+
limitsError.textContent = '';
|
|
822
|
+
});
|
|
823
|
+
limitsForm.addEventListener('submit', function (ev) {
|
|
824
|
+
ev.preventDefault();
|
|
825
|
+
limitsError.textContent = '';
|
|
826
|
+
var data = new FormData(limitsForm);
|
|
827
|
+
var submitBtn = limitsForm.querySelector('button[type="submit"]');
|
|
828
|
+
submitBtn.disabled = true;
|
|
829
|
+
var body = {
|
|
830
|
+
maxTurns: Number(data.get('maxTurns')),
|
|
831
|
+
contextBudgetTokens: Number(data.get('contextBudgetTokens')),
|
|
832
|
+
skillIndexBudgetTokens: Number(data.get('skillIndexBudgetTokens')),
|
|
833
|
+
};
|
|
834
|
+
fetch('/agents/' + encodeURIComponent(name), {
|
|
835
|
+
method: 'PUT',
|
|
836
|
+
headers: { 'content-type': 'application/json' },
|
|
837
|
+
body: JSON.stringify(body),
|
|
838
|
+
})
|
|
839
|
+
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
|
840
|
+
.then(function (result) {
|
|
841
|
+
if (!result.ok) throw new Error(result.body.error || 'request failed');
|
|
842
|
+
currentCfg = result.body;
|
|
843
|
+
overviewPanel.innerHTML = renderOverviewHtml(result.body);
|
|
844
|
+
wireOverviewHandlers(name);
|
|
845
|
+
})
|
|
846
|
+
.catch(function (err) {
|
|
847
|
+
limitsError.textContent = err.message;
|
|
848
|
+
submitBtn.disabled = false;
|
|
849
|
+
});
|
|
850
|
+
});
|
|
851
|
+
}
|
|
711
852
|
}
|
|
712
853
|
|
|
713
854
|
// ---- Overview's own Actauth section: read-only, from cfg.permissions
|
|
@@ -1016,18 +1157,194 @@ export const agentsConfigPageHtml = `<!doctype html>
|
|
|
1016
1157
|
'<button type="submit" id="skillSubmitBtn">Add</button>' +
|
|
1017
1158
|
'<button type="button" id="skillCancelBtn" style="display:none">Cancel</button>' +
|
|
1018
1159
|
'<div id="skillFormError" class="error"></div>' +
|
|
1019
|
-
'</form></section>'
|
|
1160
|
+
'</form></section>' +
|
|
1161
|
+
'<section><h3>Browse skillgarden catalog</h3>' +
|
|
1162
|
+
'<input type="text" id="skillgardenFilterInput" placeholder="Filter by name or category…">' +
|
|
1163
|
+
'<div id="skillgardenCatalogList"><p class="hint">Loading…</p></div>' +
|
|
1164
|
+
'</section>';
|
|
1020
1165
|
}
|
|
1021
1166
|
|
|
1022
1167
|
function skillsPanelEl() {
|
|
1023
1168
|
return detail.querySelector('[data-tab-panel="skills"]');
|
|
1024
1169
|
}
|
|
1025
1170
|
|
|
1171
|
+
// ---- Skills tab's "Browse skillgarden catalog" section: a
|
|
1172
|
+
// read-only, agent-independent list of everything GET /skillgarden-
|
|
1173
|
+
// catalog reports (see web/skillgarden-admin.ts) — cached in this one
|
|
1174
|
+
// module-level variable, not per-agent like gatewayLoadedFor, since
|
|
1175
|
+
// the catalog itself never varies by agent. renderSkillsTabHtml
|
|
1176
|
+
// regenerates the whole Skills panel (including this section's own
|
|
1177
|
+
// markup) on every agent switch/refresh, so loadSkillgardenCatalogSection
|
|
1178
|
+
// re-renders from this cache instead of re-fetching every time — only
|
|
1179
|
+
// the very first time this tab is opened in the page's lifetime hits
|
|
1180
|
+
// the network at all. ----
|
|
1181
|
+
var skillgardenCatalog = null;
|
|
1182
|
+
|
|
1183
|
+
function skillgardenCatalogListEl() {
|
|
1184
|
+
var panel = skillsPanelEl();
|
|
1185
|
+
return panel ? panel.querySelector('#skillgardenCatalogList') : null;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
function renderSkillgardenCatalogEntry(entry) {
|
|
1189
|
+
return '<div class="source" data-category="' + escapeHtml(entry.category) + '" data-id="' + escapeHtml(entry.id) + '">' +
|
|
1190
|
+
'<div class="source-head">' +
|
|
1191
|
+
'<h4><span class="skillgarden-category-badge">' + escapeHtml(entry.category) + '</span>' + escapeHtml(entry.id) + '</h4>' +
|
|
1192
|
+
'<span>' +
|
|
1193
|
+
'<button type="button" class="skillgarden-view-btn" data-category="' + escapeHtml(entry.category) + '" data-id="' + escapeHtml(entry.id) + '">View</button> ' +
|
|
1194
|
+
'<button type="button" class="skillgarden-add-btn" data-category="' + escapeHtml(entry.category) + '" data-id="' + escapeHtml(entry.id) + '">Add to this agent</button>' +
|
|
1195
|
+
'</span>' +
|
|
1196
|
+
'</div>' +
|
|
1197
|
+
'<p class="hint">' + escapeHtml(entry.description) + '</p>' +
|
|
1198
|
+
'<div class="skillgarden-catalog-body" style="display:none"></div>' +
|
|
1199
|
+
'</div>';
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function renderSkillgardenCatalogList(entries) {
|
|
1203
|
+
return entries.length ? entries.map(renderSkillgardenCatalogEntry).join('') : '<p class="hint">No skills match.</p>';
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function applySkillgardenCatalog(name, entries) {
|
|
1207
|
+
var listEl = skillgardenCatalogListEl();
|
|
1208
|
+
if (!listEl) return;
|
|
1209
|
+
listEl.innerHTML = renderSkillgardenCatalogList(entries);
|
|
1210
|
+
wireSkillgardenCatalogHandlers(name);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
function filteredSkillgardenCatalog(filterText) {
|
|
1214
|
+
if (!filterText) return skillgardenCatalog;
|
|
1215
|
+
var needle = filterText.toLowerCase();
|
|
1216
|
+
return skillgardenCatalog.filter(function (entry) {
|
|
1217
|
+
return entry.category.toLowerCase().indexOf(needle) !== -1 || entry.id.toLowerCase().indexOf(needle) !== -1;
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function loadSkillgardenCatalogSection(name) {
|
|
1222
|
+
var listEl = skillgardenCatalogListEl();
|
|
1223
|
+
if (!listEl) return;
|
|
1224
|
+
if (skillgardenCatalog) {
|
|
1225
|
+
var panel = skillsPanelEl();
|
|
1226
|
+
var filterInput = panel ? panel.querySelector('#skillgardenFilterInput') : null;
|
|
1227
|
+
applySkillgardenCatalog(name, filteredSkillgardenCatalog(filterInput ? filterInput.value.trim() : ''));
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
listEl.innerHTML = '<p class="hint">Loading…</p>';
|
|
1231
|
+
fetch('/skillgarden-catalog')
|
|
1232
|
+
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
|
1233
|
+
.then(function (result) {
|
|
1234
|
+
if (!result.ok) throw new Error(result.body.error || 'request failed');
|
|
1235
|
+
skillgardenCatalog = result.body;
|
|
1236
|
+
if (currentName !== name) return;
|
|
1237
|
+
applySkillgardenCatalog(name, skillgardenCatalog);
|
|
1238
|
+
})
|
|
1239
|
+
.catch(function (err) {
|
|
1240
|
+
if (currentName !== name) return;
|
|
1241
|
+
listEl.innerHTML = '<p class="error">Could not load skillgarden catalog: ' + escapeHtml(err.message) + '</p>';
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// force=true retries as an explicit overwrite after the operator
|
|
1246
|
+
// confirms — same "ask once a plain add collides, don't ask up front"
|
|
1247
|
+
// flow the rest of this page never has to think about since nothing
|
|
1248
|
+
// else lets an operator add something whose id they don't already
|
|
1249
|
+
// control the uniqueness of themselves.
|
|
1250
|
+
function performSkillgardenAdd(name, category, id, btn, originalText, force) {
|
|
1251
|
+
fetch('/agents/' + encodeURIComponent(name) + '/skillgarden-catalog/' + encodeURIComponent(category) + '/' + encodeURIComponent(id), {
|
|
1252
|
+
method: 'POST',
|
|
1253
|
+
headers: { 'content-type': 'application/json' },
|
|
1254
|
+
body: JSON.stringify({ force: !!force }),
|
|
1255
|
+
})
|
|
1256
|
+
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
|
1257
|
+
.then(function (result) {
|
|
1258
|
+
if (result.ok) {
|
|
1259
|
+
refreshSkillsDependentPanels(name);
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
if (!force && /already exists/.test(result.body.error || '')) {
|
|
1263
|
+
if (confirm('A skill named "' + id + '" already exists for this agent. Overwrite it?')) {
|
|
1264
|
+
performSkillgardenAdd(name, category, id, btn, originalText, true);
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
btn.disabled = false;
|
|
1268
|
+
btn.textContent = originalText;
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
throw new Error(result.body.error || 'request failed');
|
|
1272
|
+
})
|
|
1273
|
+
.catch(function (err) {
|
|
1274
|
+
alert('Could not add "' + id + '": ' + err.message);
|
|
1275
|
+
btn.disabled = false;
|
|
1276
|
+
btn.textContent = originalText;
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function wireSkillgardenCatalogHandlers(name) {
|
|
1281
|
+
var listEl = skillgardenCatalogListEl();
|
|
1282
|
+
if (!listEl) return;
|
|
1283
|
+
|
|
1284
|
+
var viewButtons = listEl.querySelectorAll('.skillgarden-view-btn');
|
|
1285
|
+
for (var i = 0; i < viewButtons.length; i++) {
|
|
1286
|
+
viewButtons[i].addEventListener('click', function (ev) {
|
|
1287
|
+
var btn = ev.currentTarget;
|
|
1288
|
+
var category = btn.getAttribute('data-category');
|
|
1289
|
+
var id = btn.getAttribute('data-id');
|
|
1290
|
+
var card = btn.closest('.source');
|
|
1291
|
+
var bodyEl = card.querySelector('.skillgarden-catalog-body');
|
|
1292
|
+
if (bodyEl.style.display !== 'none') {
|
|
1293
|
+
bodyEl.style.display = 'none';
|
|
1294
|
+
btn.textContent = 'View';
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
if (bodyEl.getAttribute('data-loaded') === '1') {
|
|
1298
|
+
bodyEl.style.display = 'block';
|
|
1299
|
+
btn.textContent = 'Hide';
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
btn.disabled = true;
|
|
1303
|
+
fetch('/skillgarden-catalog/' + encodeURIComponent(category) + '/' + encodeURIComponent(id))
|
|
1304
|
+
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
|
1305
|
+
.then(function (result) {
|
|
1306
|
+
if (!result.ok) throw new Error(result.body.error || 'request failed');
|
|
1307
|
+
bodyEl.innerHTML = renderMarkdownPreview(result.body.body);
|
|
1308
|
+
bodyEl.setAttribute('data-loaded', '1');
|
|
1309
|
+
bodyEl.style.display = 'block';
|
|
1310
|
+
btn.textContent = 'Hide';
|
|
1311
|
+
})
|
|
1312
|
+
.catch(function (err) {
|
|
1313
|
+
bodyEl.innerHTML = '<p class="error">' + escapeHtml(err.message) + '</p>';
|
|
1314
|
+
bodyEl.style.display = 'block';
|
|
1315
|
+
})
|
|
1316
|
+
.then(function () { btn.disabled = false; });
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
var addButtons = listEl.querySelectorAll('.skillgarden-add-btn');
|
|
1321
|
+
for (var j = 0; j < addButtons.length; j++) {
|
|
1322
|
+
addButtons[j].addEventListener('click', function (ev) {
|
|
1323
|
+
var btn = ev.currentTarget;
|
|
1324
|
+
var category = btn.getAttribute('data-category');
|
|
1325
|
+
var id = btn.getAttribute('data-id');
|
|
1326
|
+
var originalText = btn.textContent;
|
|
1327
|
+
btn.disabled = true;
|
|
1328
|
+
btn.textContent = 'Adding…';
|
|
1329
|
+
performSkillgardenAdd(name, category, id, btn, originalText, false);
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1026
1334
|
function wireSkillsHandlers(name) {
|
|
1027
1335
|
var panel = skillsPanelEl();
|
|
1028
1336
|
if (!panel) return;
|
|
1029
1337
|
var editingSkillId = null;
|
|
1030
1338
|
|
|
1339
|
+
loadSkillgardenCatalogSection(name);
|
|
1340
|
+
var skillgardenFilterInput = panel.querySelector('#skillgardenFilterInput');
|
|
1341
|
+
if (skillgardenFilterInput) {
|
|
1342
|
+
skillgardenFilterInput.addEventListener('input', function () {
|
|
1343
|
+
if (!skillgardenCatalog) return;
|
|
1344
|
+
applySkillgardenCatalog(name, filteredSkillgardenCatalog(skillgardenFilterInput.value.trim()));
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1031
1348
|
// Write/Preview toggle for the Body field — same idea as GitHub's own
|
|
1032
1349
|
// issue/PR editor: the textarea and the rendered preview show/hide
|
|
1033
1350
|
// each other, the textarea's value is the only source of truth (the
|
|
@@ -1158,7 +1475,7 @@ export const agentsConfigPageHtml = `<!doctype html>
|
|
|
1158
1475
|
|
|
1159
1476
|
function renderToolsTabHtml(cfg) {
|
|
1160
1477
|
return '<section><h3>Local tools (' + cfg.localTools.length + ')</h3>' + renderTools(cfg.localTools) +
|
|
1161
|
-
'<div class="
|
|
1478
|
+
'<div class="admin-subsection"><h4>Create an HTTP tool</h4>' + renderHttpToolFormHtml() + '</div>' +
|
|
1162
1479
|
'</section>' +
|
|
1163
1480
|
'<section id="gatewayToolsSection">' +
|
|
1164
1481
|
'<h3>Gateway Tools <button type="button" id="gatewayToolsRefreshBtn" class="hint-btn">Refresh</button></h3>' +
|
|
@@ -1207,7 +1524,7 @@ export const agentsConfigPageHtml = `<!doctype html>
|
|
|
1207
1524
|
'<input type="text" name="name" required pattern="[a-z][a-z0-9_]*" placeholder="lookup_order_status">' +
|
|
1208
1525
|
'</label>' +
|
|
1209
1526
|
'<label>Description' +
|
|
1210
|
-
'<
|
|
1527
|
+
'<textarea name="description" id="httpToolDescriptionInput" required placeholder="What this tool does"></textarea>' +
|
|
1211
1528
|
'</label>' +
|
|
1212
1529
|
'<label>Fields' +
|
|
1213
1530
|
'<div id="httpToolFieldRows"></div>' +
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentConfig } from '../core/agent-config.js';
|
|
2
2
|
export interface ModelProviderInfo {
|
|
3
|
-
provider: 'anthropic' | 'openai' | 'deepseek';
|
|
3
|
+
provider: 'anthropic' | 'openai' | 'deepseek' | 'kimi' | 'glm' | 'gemini';
|
|
4
4
|
envVar: string;
|
|
5
5
|
configured: boolean;
|
|
6
6
|
}
|
|
@@ -8,6 +8,11 @@ const KNOWN_PROVIDERS = [
|
|
|
8
8
|
{ provider: 'anthropic', envVar: 'ANTHROPIC_API_KEY' },
|
|
9
9
|
{ provider: 'openai', envVar: 'OPENAI_API_KEY' },
|
|
10
10
|
{ provider: 'deepseek', envVar: 'DEEPSEEK_API_KEY' },
|
|
11
|
+
// Moonshot AI's own env var name, not KIMI_API_KEY — see
|
|
12
|
+
// core/model-calls/kimi-model-call.ts's own header comment for why.
|
|
13
|
+
{ provider: 'kimi', envVar: 'MOONSHOT_API_KEY' },
|
|
14
|
+
{ provider: 'glm', envVar: 'GLM_API_KEY' },
|
|
15
|
+
{ provider: 'gemini', envVar: 'GEMINI_API_KEY' },
|
|
11
16
|
];
|
|
12
17
|
/** Every registered agent's resolved model, alongside whether each
|
|
13
18
|
* supported provider actually has credentials available in this
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare class SkillgardenUnavailableError extends Error {
|
|
2
|
+
}
|
|
3
|
+
export interface SkillgardenCatalogEntry {
|
|
4
|
+
category: string;
|
|
5
|
+
id: string;
|
|
6
|
+
description: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SkillgardenCatalogDetail extends SkillgardenCatalogEntry {
|
|
9
|
+
body: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function listSkillgardenCatalog(): SkillgardenCatalogEntry[];
|
|
12
|
+
export declare function readSkillgardenCatalogEntry(category: string, id: string): SkillgardenCatalogDetail;
|
|
13
|
+
/** Copies `registry/<category>/<id>/` into `agents/<agentName>/skills/<id>/`
|
|
14
|
+
* — delegates entirely to skillgarden's own `addSkill`, which already
|
|
15
|
+
* knows this exact `agents/<agent>/skills` destination convention (see
|
|
16
|
+
* its own doc comment) and already refuses (or, with `force`,
|
|
17
|
+
* overwrites) an existing destination. Returns the bare skill id actually
|
|
18
|
+
* installed — never category-qualified, matching `addSkill`'s own
|
|
19
|
+
* "category is a registry-organization concept, not forced onto what a
|
|
20
|
+
* caller now owns" convention. */
|
|
21
|
+
export declare function addSkillgardenSkillToAgent(agentName: string, category: string, id: string, force?: boolean): {
|
|
22
|
+
id: string;
|
|
23
|
+
path: string;
|
|
24
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Lets an operator browse skillgarden's own bundled skill registry from
|
|
2
|
+
// the Skills tab in /agents/config, and add one straight into an agent's
|
|
3
|
+
// agents/<name>/skills/ folder — a shortcut for the same thing
|
|
4
|
+
// `npx skillgarden add <skill> --agent <name>` already does from a
|
|
5
|
+
// terminal, surfaced in the UI instead. This module only ever *reads*
|
|
6
|
+
// the registry and delegates the actual copy to skillgarden's own
|
|
7
|
+
// exported `addSkill` (see below) — it never re-implements that logic.
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
import { SkillGarden, addSkill as skillgardenAddSkill } from 'skillgarden';
|
|
11
|
+
export class SkillgardenUnavailableError extends Error {
|
|
12
|
+
}
|
|
13
|
+
// skillgarden's own CLI resolves its bundled registry/ relative to its
|
|
14
|
+
// own compiled dist/cli.js location (see its cli.ts's own
|
|
15
|
+
// DEFAULT_REGISTRY_DIR), but doesn't export that path — `addSkill`
|
|
16
|
+
// itself defaults to it internally, which is enough for *installing* a
|
|
17
|
+
// skill, but browsing/previewing the catalog needs the directory path
|
|
18
|
+
// itself. Re-derived here the same way Node's own module resolution
|
|
19
|
+
// would: from package.json's real installed location, not assumed
|
|
20
|
+
// relative to this file (skillgarden is a real npm dependency, not a
|
|
21
|
+
// vendored copy).
|
|
22
|
+
function registryDir() {
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
let pkgJsonPath;
|
|
25
|
+
try {
|
|
26
|
+
pkgJsonPath = require.resolve('skillgarden/package.json');
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw new SkillgardenUnavailableError('skillgarden is not installed — run `npm install skillgarden` to enable the skill catalog.');
|
|
30
|
+
}
|
|
31
|
+
return join(dirname(pkgJsonPath), 'registry');
|
|
32
|
+
}
|
|
33
|
+
// A large, fixed budget, not run-agent.ts's own live skillIndexBudgetTokens
|
|
34
|
+
// default — this indexes a small, fixed, bundled catalog for browsing,
|
|
35
|
+
// not an agent's own live context, so nothing here should ever silently
|
|
36
|
+
// truncate the list an operator sees.
|
|
37
|
+
const CATALOG_INDEX_BUDGET_TOKENS = 1_000_000;
|
|
38
|
+
// discoverSkillFiles namespaces registry/<category>/<skill>/SKILL.md as
|
|
39
|
+
// "<category>:<skill>" (colon-joined, one level of nesting) — split on
|
|
40
|
+
// just the first colon, not skillId itself, in case a future entry ever
|
|
41
|
+
// nests deeper than one category level.
|
|
42
|
+
function splitNamespacedName(namespacedName) {
|
|
43
|
+
const colon = namespacedName.indexOf(':');
|
|
44
|
+
return { category: namespacedName.slice(0, colon), id: namespacedName.slice(colon + 1) };
|
|
45
|
+
}
|
|
46
|
+
function catalogGarden() {
|
|
47
|
+
return new SkillGarden({ dirs: [registryDir()], indexBudgetTokens: CATALOG_INDEX_BUDGET_TOKENS });
|
|
48
|
+
}
|
|
49
|
+
export function listSkillgardenCatalog() {
|
|
50
|
+
const { included } = catalogGarden().buildIndex();
|
|
51
|
+
return included
|
|
52
|
+
.map((entry) => ({ ...splitNamespacedName(entry.name), description: entry.description }))
|
|
53
|
+
.sort((a, b) => (a.category === b.category ? a.id.localeCompare(b.id) : a.category.localeCompare(b.category)));
|
|
54
|
+
}
|
|
55
|
+
export function readSkillgardenCatalogEntry(category, id) {
|
|
56
|
+
const garden = catalogGarden();
|
|
57
|
+
garden.buildIndex();
|
|
58
|
+
// load() throws a plain Error for an unindexed name — same "not found"
|
|
59
|
+
// shape a bad category/id combination should surface as; the caller
|
|
60
|
+
// (adapters/http.ts) maps any throw here to 404, so there's nothing
|
|
61
|
+
// more specific to catch and re-throw.
|
|
62
|
+
const loaded = garden.load(`${category}:${id}`);
|
|
63
|
+
return { category, id, description: loaded.description, body: loaded.body };
|
|
64
|
+
}
|
|
65
|
+
/** Copies `registry/<category>/<id>/` into `agents/<agentName>/skills/<id>/`
|
|
66
|
+
* — delegates entirely to skillgarden's own `addSkill`, which already
|
|
67
|
+
* knows this exact `agents/<agent>/skills` destination convention (see
|
|
68
|
+
* its own doc comment) and already refuses (or, with `force`,
|
|
69
|
+
* overwrites) an existing destination. Returns the bare skill id actually
|
|
70
|
+
* installed — never category-qualified, matching `addSkill`'s own
|
|
71
|
+
* "category is a registry-organization concept, not forced onto what a
|
|
72
|
+
* caller now owns" convention. */
|
|
73
|
+
export function addSkillgardenSkillToAgent(agentName, category, id, force = false) {
|
|
74
|
+
const result = skillgardenAddSkill({ skill: `${category}/${id}`, agent: agentName, registryDir: registryDir(), force });
|
|
75
|
+
return { id, path: join(result.destination, 'SKILL.md') };
|
|
76
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loopengine",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Runtime for tool-using AI agents with a transparent ReAct loop.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,6 +41,12 @@
|
|
|
41
41
|
"dist/core/model-calls/openai-model-call.d.ts",
|
|
42
42
|
"dist/core/model-calls/deepseek-model-call.js",
|
|
43
43
|
"dist/core/model-calls/deepseek-model-call.d.ts",
|
|
44
|
+
"dist/core/model-calls/kimi-model-call.js",
|
|
45
|
+
"dist/core/model-calls/kimi-model-call.d.ts",
|
|
46
|
+
"dist/core/model-calls/glm-model-call.js",
|
|
47
|
+
"dist/core/model-calls/glm-model-call.d.ts",
|
|
48
|
+
"dist/core/model-calls/gemini-model-call.js",
|
|
49
|
+
"dist/core/model-calls/gemini-model-call.d.ts",
|
|
44
50
|
"dist/core/discover-agents.js",
|
|
45
51
|
"dist/core/discover-agents.d.ts",
|
|
46
52
|
"dist/core/agent-as-tool.js",
|
|
@@ -78,7 +84,7 @@
|
|
|
78
84
|
"mcpplug": "^0.0.1",
|
|
79
85
|
"openai": "^7.4.0",
|
|
80
86
|
"sessionknit": "^0.0.2",
|
|
81
|
-
"skillgarden": "^0.0.
|
|
87
|
+
"skillgarden": "^0.0.9",
|
|
82
88
|
"toollane": "^0.0.1",
|
|
83
89
|
"typescript": "^5.7.0",
|
|
84
90
|
"yaml": "^2.6.0"
|