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.
@@ -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 to
3
- // agents/ that exports `config` and `createModelCall` — nothing here or
4
- // in any adapter needs to change; discoverAgents scans that directory and
5
- // keys each entry by AgentConfig.name (not the filename) at import time.
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
- const agentsDir = join(dirname(fileURLToPath(import.meta.url)), 'agents')
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]
@@ -9,7 +9,9 @@
9
9
  "cli": "tsx adapters/cli.ts"
10
10
  },
11
11
  "dependencies": {
12
- "loopengine": "^0.1.2"
12
+ "loopengine": "^0.1.4",
13
+ "actauth": "^0.0.12",
14
+ "skillgarden": "^0.0.5"
13
15
  },
14
16
  "devDependencies": {
15
17
  "@types/node": "^22.10.0",
@@ -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
- }