create-loopengine 0.1.8 → 0.1.11
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/package.json +1 -1
- package/template/.env.example +8 -2
- package/template/README.md +40 -13
- package/template/adapters/cli.ts +70 -11
- package/template/adapters/http.ts +1726 -36
- package/template/agent-registry.ts +58 -6
- package/template/agents/example-agent/actauth.yml +12 -0
- package/template/agents/example-agent/index.ts +29 -0
- package/template/agents/example-agent/skills/convert-temperature/SKILL.md +11 -0
- package/template/agents/example-agent/tools/get_weather.ts +8 -0
- package/template/agents/example-agent/tools/index.ts +12 -0
- package/template/package.json +3 -1
- package/template/agents/example-agent.ts +0 -27
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
// Every adapter resolves "which agent" through this — the thing that
|
|
2
|
-
// makes agents pluggable. Defining a new agent means adding a file
|
|
3
|
-
// agents/ that exports `config` and `createModelCall` —
|
|
4
|
-
// in any adapter needs to change; discoverAgents scans
|
|
5
|
-
// keys each entry by AgentConfig.name (not the
|
|
2
|
+
// makes agents pluggable. Defining a new agent means adding a file (or a
|
|
3
|
+
// folder) to agents/ that exports `config` and `createModelCall` —
|
|
4
|
+
// nothing here or in any adapter needs to change; discoverAgents scans
|
|
5
|
+
// that directory and keys each entry by AgentConfig.name (not the
|
|
6
|
+
// filename) at import time.
|
|
6
7
|
import { dirname, join } from 'node:path'
|
|
7
8
|
import { fileURLToPath } from 'node:url'
|
|
8
|
-
import { discoverAgents, type AgentModule } from 'loopengine'
|
|
9
|
+
import { discoverAgents, type AgentConfig, type AgentModule, type ModelCall } from 'loopengine'
|
|
9
10
|
|
|
10
11
|
export type RegistryEntry = AgentModule
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
// This file lives at the project root, alongside agents/ — no '..' needed
|
|
14
|
+
// (unlike loopengine's own core/agent-registry.ts, which lives nested one
|
|
15
|
+
// level down in core/).
|
|
16
|
+
const projectRoot = dirname(fileURLToPath(import.meta.url))
|
|
17
|
+
const agentsDir = join(projectRoot, 'agents')
|
|
13
18
|
|
|
14
19
|
// Top-level await: ESM guarantees an importer's own evaluation (adapters/
|
|
15
20
|
// http.ts, adapters/cli.ts) waits for this module's top-level await to
|
|
@@ -25,3 +30,50 @@ export function listAgents(): string[] {
|
|
|
25
30
|
export function getEntry(name: string): RegistryEntry | undefined {
|
|
26
31
|
return entries.get(name)
|
|
27
32
|
}
|
|
33
|
+
|
|
34
|
+
/** Where this registry's own discoverAgents call resolved agents/
|
|
35
|
+
* against — exported so a caller creating a *new* agent module (the
|
|
36
|
+
* Admin UI's "Create new agent" flow, via loopengine's own scaffoldAgent)
|
|
37
|
+
* writes it to the exact same place this registry would find it on a
|
|
38
|
+
* future restart, rather than re-deriving a base dir independently (e.g.
|
|
39
|
+
* from process.cwd(), which isn't guaranteed to match if the process is
|
|
40
|
+
* launched from elsewhere) and risking the two drifting apart. */
|
|
41
|
+
export function projectDir(): string {
|
|
42
|
+
return projectRoot
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Adds one already-loaded agent module to the live registry in place —
|
|
46
|
+
* how a newly created agent becomes runnable in *this* process
|
|
47
|
+
* immediately, with no restart — unlike discoverAgents' own directory
|
|
48
|
+
* scan, which only ever runs once, at this module's own import time (see
|
|
49
|
+
* the top-level await above). Throws on a name collision with an
|
|
50
|
+
* already-registered agent, same as discoverAgents itself does for a
|
|
51
|
+
* collision found during its initial scan — a duplicate AgentConfig.name
|
|
52
|
+
* is a bug either way, not something to silently overwrite. */
|
|
53
|
+
export function registerAgent(agentModule: RegistryEntry): void {
|
|
54
|
+
const name = agentModule.config.name
|
|
55
|
+
if (entries.has(name)) {
|
|
56
|
+
throw new Error(`Duplicate agent name '${name}' — an agent with this name is already registered.`)
|
|
57
|
+
}
|
|
58
|
+
entries.set(name, agentModule)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Applies a partial update to an already-registered agent — how an edit
|
|
62
|
+
* made through the Admin UI takes effect in *this* running process
|
|
63
|
+
* immediately, the same "no restart needed" principle gateway-tools.yml/
|
|
64
|
+
* actauth.yml/SKILL.md edits already have. Mutates the same AgentConfig
|
|
65
|
+
* object every existing getEntry() caller already holds a reference to —
|
|
66
|
+
* via Object.assign, not a full replacement — so nothing needs to
|
|
67
|
+
* re-fetch it to see the change, and any field left out of `patch.config`
|
|
68
|
+
* (tools, rules, skillsDirs, ...) stays exactly as it was.
|
|
69
|
+
* `patch.createModelCall`, if given, replaces the cached one wholesale.
|
|
70
|
+
* Throws if `name` isn't already registered — this updates an existing
|
|
71
|
+
* entry, it's not a back door around registerAgent's own creation path. */
|
|
72
|
+
export function updateAgent(name: string, patch: { config?: Partial<AgentConfig>; createModelCall?: () => ModelCall }): void {
|
|
73
|
+
const entry = entries.get(name)
|
|
74
|
+
if (!entry) {
|
|
75
|
+
throw new Error(`No registered agent named '${name}' to update.`)
|
|
76
|
+
}
|
|
77
|
+
if (patch.config) Object.assign(entry.config, patch.config)
|
|
78
|
+
if (patch.createModelCall) entry.createModelCall = patch.createModelCall
|
|
79
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# The permission story for this agent: which tool, which rule, allow or
|
|
2
|
+
# ask. Loaded automatically because index.ts doesn't set `rules` itself —
|
|
3
|
+
# omitting it defaults to this file, agents/<name>/actauth.yml, resolved
|
|
4
|
+
# from wherever you run the process (see AgentConfig.rules's own doc
|
|
5
|
+
# comment in loopengine for the full resolution rules).
|
|
6
|
+
default_decision: ask
|
|
7
|
+
|
|
8
|
+
rules:
|
|
9
|
+
- name: get-weather-always-allowed
|
|
10
|
+
scope: "*/*"
|
|
11
|
+
tool: get_weather
|
|
12
|
+
decision: allow
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// A starting point, not a template to copy verbatim: real persona, real
|
|
2
|
+
// tool, real permission rule — one folder per agent, the same convention
|
|
3
|
+
// agents/customer-service/ uses in loopengine's own repo. Add more agents
|
|
4
|
+
// by copying this whole folder under a new name and a new AgentConfig.name
|
|
5
|
+
// — agent-registry.ts discovers it automatically, nothing to register by
|
|
6
|
+
// hand.
|
|
7
|
+
//
|
|
8
|
+
// `rules` and `skillsDirs` are both left unset here on purpose — omitting
|
|
9
|
+
// them defaults to actauth.yml and skills/ alongside this file (loopengine
|
|
10
|
+
// resolves both relative to wherever you run the process). `tools` is set
|
|
11
|
+
// explicitly instead of relying on the equivalent tools/ default: that one
|
|
12
|
+
// resolves relative to loopengine's own installed location, not your
|
|
13
|
+
// project, so it only works inside loopengine's own repo — see
|
|
14
|
+
// tools/index.ts's own comment.
|
|
15
|
+
import { createAnthropicModelCall, type AgentConfig, type ModelCall } from 'loopengine'
|
|
16
|
+
import { tools } from './tools/index.js'
|
|
17
|
+
|
|
18
|
+
export const config: AgentConfig = {
|
|
19
|
+
name: 'example-agent',
|
|
20
|
+
systemPrompt: 'You are a helpful assistant.',
|
|
21
|
+
tools,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Reads ANTHROPIC_API_KEY from the env. Swap for
|
|
25
|
+
// createOpenAIModelCall({ model: '...' }) to use OpenAI instead — see
|
|
26
|
+
// node_modules/loopengine/README.md's "Wiring a real model" section.
|
|
27
|
+
export function createModelCall(): ModelCall {
|
|
28
|
+
return createAnthropicModelCall({ model: 'claude-sonnet-5' })
|
|
29
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: convert-temperature
|
|
3
|
+
description: Convert a temperature between Celsius and Fahrenheit without a tool call.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Convert temperature
|
|
7
|
+
|
|
8
|
+
To convert Celsius to Fahrenheit: multiply by 9/5, then add 32.
|
|
9
|
+
To convert Fahrenheit to Celsius: subtract 32, then multiply by 5/9.
|
|
10
|
+
|
|
11
|
+
Do the arithmetic yourself and show the result — no tool needed for this.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ToolDefinition } from 'loopengine'
|
|
2
|
+
|
|
3
|
+
export const getWeather: ToolDefinition = {
|
|
4
|
+
name: 'get_weather',
|
|
5
|
+
description: 'Look up the weather for a city',
|
|
6
|
+
input_schema: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] },
|
|
7
|
+
execute: async (input) => `Sunny in ${input.city}`,
|
|
8
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Aggregates this agent's tools — one file per tool, so adding another
|
|
2
|
+
// tool later means adding a file here and one line below, not touching
|
|
3
|
+
// index.ts. Mirrors loopengine's own agents/customer-service/tools/index.ts,
|
|
4
|
+
// except index.ts here imports this file explicitly (`tools,` in its
|
|
5
|
+
// AgentConfig) rather than omitting `tools` and relying on loopengine's
|
|
6
|
+
// own auto-import of this exact path: that default is resolved relative
|
|
7
|
+
// to loopengine's own installed location, so it only finds a tools/
|
|
8
|
+
// folder that lives inside loopengine's own repo, not your project's.
|
|
9
|
+
import type { ToolDefinition } from 'loopengine'
|
|
10
|
+
import { getWeather } from './get_weather.js'
|
|
11
|
+
|
|
12
|
+
export const tools: ToolDefinition[] = [getWeather]
|
package/template/package.json
CHANGED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
// A starting point, not a template to copy verbatim: real persona, real
|
|
2
|
-
// tool, real permission rule. Add more agents by copying this file into
|
|
3
|
-
// agents/ under a new filename and a new AgentConfig.name — agent-registry.ts
|
|
4
|
-
// discovers it automatically, nothing to register by hand.
|
|
5
|
-
import { createAnthropicModelCall, type AgentConfig, type ModelCall } from 'loopengine'
|
|
6
|
-
|
|
7
|
-
export const config: AgentConfig = {
|
|
8
|
-
name: 'example-agent',
|
|
9
|
-
systemPrompt: 'You are a helpful assistant.',
|
|
10
|
-
tools: [
|
|
11
|
-
{
|
|
12
|
-
name: 'get_weather',
|
|
13
|
-
description: 'Look up the weather for a city',
|
|
14
|
-
input_schema: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] },
|
|
15
|
-
execute: async (input) => `Sunny in ${input.city}`,
|
|
16
|
-
},
|
|
17
|
-
],
|
|
18
|
-
rules: [{ scopePattern: 'default/production/example-agent', tool: 'get_weather', decision: 'allow' }],
|
|
19
|
-
defaultDecision: 'ask',
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Reads ANTHROPIC_API_KEY from the env. Swap for
|
|
23
|
-
// createOpenAIModelCall({ model: '...' }) to use OpenAI instead — see
|
|
24
|
-
// node_modules/loopengine/README.md's "Wiring a real model" section.
|
|
25
|
-
export function createModelCall(): ModelCall {
|
|
26
|
-
return createAnthropicModelCall({ model: 'claude-sonnet-5' })
|
|
27
|
-
}
|