create-loopengine 0.1.9 → 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 +17 -0
- package/template/adapters/cli.ts +70 -11
- package/template/adapters/http.ts +1726 -36
- package/template/agent-registry.ts +58 -6
- package/template/package.json +3 -1
package/package.json
CHANGED
package/template/.env.example
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
ANTHROPIC_API_KEY=
|
|
2
|
-
# Using OpenAI instead? Swap agents/example-agent.ts's createModelCall
|
|
3
|
-
# createOpenAIModelCall({ model: '...' }) and set this instead:
|
|
2
|
+
# Using OpenAI instead? Swap agents/example-agent/index.ts's createModelCall
|
|
3
|
+
# for createOpenAIModelCall({ model: '...' }) and set this instead:
|
|
4
4
|
# OPENAI_API_KEY=
|
|
5
5
|
|
|
6
6
|
PORT=8787
|
|
@@ -8,3 +8,9 @@ PORT=8787
|
|
|
8
8
|
# Optional — omit for local, file-based sessions under .sessions/. Set this
|
|
9
9
|
# once you run more than one server instance against the same sessions.
|
|
10
10
|
# REDIS_URL=redis://localhost:6379
|
|
11
|
+
|
|
12
|
+
# Optional, but set this before deploying anywhere reachable by anyone but
|
|
13
|
+
# you: without it, every route on this server — including each agent's
|
|
14
|
+
# conversation history — is wide open to anyone who can reach it.
|
|
15
|
+
# "user:pass" turns on HTTP Basic Auth for all of them.
|
|
16
|
+
# LOOPENGINE_ADMIN_AUTH=user:pass
|
package/template/README.md
CHANGED
|
@@ -31,6 +31,23 @@ Or from the command line directly, no server needed:
|
|
|
31
31
|
npm run cli -- --agent example-agent "what is the weather in Boston?"
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
## Admin UI
|
|
35
|
+
|
|
36
|
+
`npm run dev`, then open:
|
|
37
|
+
|
|
38
|
+
- `http://localhost:8787/agents/config` — create agents, edit system
|
|
39
|
+
prompts and models, connect Composio tool sources, and manage each
|
|
40
|
+
agent's permission rules and skills, all from the browser. "Create new
|
|
41
|
+
agent" writes a real folder under `agents/` and registers it live, no
|
|
42
|
+
restart needed.
|
|
43
|
+
- `http://localhost:8787/playground` — send a real message to any agent
|
|
44
|
+
and watch its tool calls and permission decisions happen live, approve
|
|
45
|
+
or deny a gated call right there.
|
|
46
|
+
|
|
47
|
+
Both are wide open by default (local-only convenience) — set
|
|
48
|
+
`LOOPENGINE_ADMIN_AUTH="user:pass"` in `.env` before deploying anywhere
|
|
49
|
+
reachable by anyone but you (see `.env.example`).
|
|
50
|
+
|
|
34
51
|
## Add another agent
|
|
35
52
|
|
|
36
53
|
1. Copy `agents/example-agent/` to `agents/your-agent/` — `index.ts`
|
package/template/adapters/cli.ts
CHANGED
|
@@ -3,51 +3,110 @@
|
|
|
3
3
|
//
|
|
4
4
|
// npm run cli -- --agent example-agent "what's the weather in Boston?"
|
|
5
5
|
//
|
|
6
|
-
// Omit --session for a one-off ask — each call gets its own fresh,
|
|
6
|
+
// Omit --session for a one-off ask — each such call gets its own fresh,
|
|
7
7
|
// isolated session (a random id, printed to stderr so it can be reused
|
|
8
|
-
// with --session <id> later to continue that
|
|
8
|
+
// with --session <id> later if you want to continue that exact
|
|
9
|
+
// conversation). Pass --session explicitly to have two calls share one
|
|
10
|
+
// ongoing conversation on purpose.
|
|
11
|
+
//
|
|
12
|
+
// npm run cli -- --agent example-agent --json "what's the weather in Boston?"
|
|
13
|
+
//
|
|
14
|
+
// --json prints the same typed LoopEvent stream adapters/http.ts's SSE
|
|
15
|
+
// route sends as one JSON object per line on stdout, ending with a
|
|
16
|
+
// 'done'/'error' event — nothing human-readable mixed in, so it's
|
|
17
|
+
// pipeable straight into `jq` or another process. Without --json, stdout
|
|
18
|
+
// stays human-oriented (just the final answer text) and events go to
|
|
19
|
+
// stderr as a one-line summary each.
|
|
20
|
+
//
|
|
21
|
+
// Owns: parsing argv, loading/saving this session's history, printing the
|
|
22
|
+
// result to stdout. Everything else is runAgent. Uses REDIS_URL if set
|
|
23
|
+
// (see loopengine's own createSessionStore), a local .sessions/ dir
|
|
24
|
+
// otherwise — either way, safe for concurrent invocations against the
|
|
25
|
+
// same --session.
|
|
9
26
|
import { randomUUID } from 'node:crypto'
|
|
10
|
-
import { createSessionStore, runAgent } from 'loopengine'
|
|
27
|
+
import { createSessionStore, runAgent, type LoopEvent } from 'loopengine'
|
|
11
28
|
import { getEntry, listAgents } from '../agent-registry.js'
|
|
12
29
|
|
|
13
30
|
function parseArgs(argv: string[]) {
|
|
14
31
|
let agent = ''
|
|
15
32
|
let session: string | undefined
|
|
33
|
+
let json = false
|
|
16
34
|
const rest: string[] = []
|
|
17
35
|
for (let i = 0; i < argv.length; i++) {
|
|
18
36
|
if (argv[i] === '--agent') agent = argv[++i]
|
|
19
37
|
else if (argv[i] === '--session') session = argv[++i]
|
|
38
|
+
else if (argv[i] === '--json') json = true
|
|
20
39
|
else rest.push(argv[i])
|
|
21
40
|
}
|
|
22
|
-
return { agent, session, message: rest.join(' ') }
|
|
41
|
+
return { agent, session, json, message: rest.join(' ') }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** One NDJSON line per event — see this file's own header comment for
|
|
45
|
+
* why --json mode never mixes human-readable text into stdout. */
|
|
46
|
+
function printJsonEvent(event: LoopEvent): void {
|
|
47
|
+
console.log(JSON.stringify(event))
|
|
23
48
|
}
|
|
24
49
|
|
|
25
50
|
async function main() {
|
|
26
|
-
const { agent, session, message } = parseArgs(process.argv.slice(2))
|
|
51
|
+
const { agent, session, json, message } = parseArgs(process.argv.slice(2))
|
|
27
52
|
const entry = getEntry(agent)
|
|
28
53
|
if (!entry) {
|
|
29
54
|
console.error(`unknown agent '${agent}'. available: ${listAgents().join(', ')}`)
|
|
30
55
|
process.exit(1)
|
|
31
56
|
}
|
|
32
57
|
if (!message) {
|
|
33
|
-
console.error('usage: cli.ts --agent <name> [--session <id>] "<message>"')
|
|
58
|
+
console.error('usage: cli.ts --agent <name> [--session <id>] [--json] "<message>"')
|
|
34
59
|
process.exit(1)
|
|
35
60
|
}
|
|
36
61
|
|
|
62
|
+
// No --session means "a fresh, one-off ask," not "join whatever the
|
|
63
|
+
// default conversation happens to be" — a shared 'default' bucket would
|
|
64
|
+
// silently accumulate history across every untagged invocation forever,
|
|
65
|
+
// splicing unrelated one-off asks into a single ever-growing session.
|
|
37
66
|
const sessionId = session ?? randomUUID()
|
|
38
|
-
|
|
67
|
+
// In --json mode this is the stream's own first line (mirroring
|
|
68
|
+
// adapters/http.ts's SSE 'session' event) — not also duplicated to
|
|
69
|
+
// stderr, so a consumer piping stdout gets exactly one place to learn
|
|
70
|
+
// the generated id from, same as an SSE client does.
|
|
71
|
+
if (json) printJsonEvent({ type: 'session', sessionId })
|
|
72
|
+
else if (!session) console.error(`[session] ${sessionId} (pass --session ${sessionId} to continue this conversation)`)
|
|
39
73
|
|
|
40
74
|
const sessions = createSessionStore()
|
|
41
75
|
try {
|
|
42
|
-
//
|
|
43
|
-
// different agents
|
|
76
|
+
// SessionStore itself is agent-agnostic — the same --session id reused
|
|
77
|
+
// across two different agents would otherwise read and write the same
|
|
78
|
+
// underlying log (see adapters/http.ts's identical fix). Namespacing
|
|
79
|
+
// by agent name here is what actually keeps them isolated.
|
|
44
80
|
const text = await sessions.withSession(`${agent}:${sessionId}`, async (history) => {
|
|
45
81
|
const result = await runAgent(entry.config, entry.createModelCall(), message, history, {
|
|
46
|
-
|
|
82
|
+
channel: 'cli',
|
|
83
|
+
onEvent: json
|
|
84
|
+
? printJsonEvent
|
|
85
|
+
: (event) => {
|
|
86
|
+
const { type, ...detail } = event
|
|
87
|
+
console.error(`[${type}]`, detail)
|
|
88
|
+
},
|
|
47
89
|
})
|
|
90
|
+
// Same synthetic 'done' event adapters/http.ts's streaming route
|
|
91
|
+
// sends once a turn is over — covers a genuine finish and both
|
|
92
|
+
// synthetic stopReasons alike, so a --json consumer always sees
|
|
93
|
+
// exactly one terminal event regardless of how the turn actually
|
|
94
|
+
// ended.
|
|
95
|
+
if (json) printJsonEvent({ type: 'done', text: result.text, ...(result.stopReason ? { stopReason: result.stopReason } : {}) })
|
|
48
96
|
return { newMessages: result.newMessages, result: result.text }
|
|
49
97
|
})
|
|
50
|
-
console.log(text)
|
|
98
|
+
if (!json) console.log(text)
|
|
99
|
+
} catch (err) {
|
|
100
|
+
// Mirrors adapters/http.ts's own in-band 'error' SSE event — in
|
|
101
|
+
// --json mode the turn may already have written partial output to
|
|
102
|
+
// stdout, so the failure needs to land there too, not just as an
|
|
103
|
+
// uncaught rejection's stack trace on stderr.
|
|
104
|
+
if (json) {
|
|
105
|
+
printJsonEvent({ type: 'error', error: err instanceof Error ? err.message : String(err) })
|
|
106
|
+
process.exitCode = 1
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
throw err
|
|
51
110
|
} finally {
|
|
52
111
|
await sessions.close()
|
|
53
112
|
}
|