loopengine 0.0.17 → 0.0.20
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/README.md +67 -6
- package/core/system-skills/composio-large-outputs/SKILL.md +25 -0
- package/dist/{cli.d.ts → bin/cli.d.ts} +27 -6
- package/dist/bin/cli.js +384 -0
- package/dist/{agent-as-tool.d.ts → core/agent-as-tool.d.ts} +2 -2
- package/dist/{agent-as-tool.js → core/agent-as-tool.js} +1 -1
- package/dist/core/client.d.ts +113 -0
- package/dist/core/client.js +123 -0
- package/dist/core/discover-agents.d.ts +45 -0
- package/dist/{discover-agents.js → core/discover-agents.js} +12 -4
- package/dist/core/gateway-tools.d.ts +226 -0
- package/dist/core/gateway-tools.js +660 -0
- package/dist/core/loop-events.d.ts +168 -0
- package/dist/core/loop-events.js +1 -0
- package/dist/{model-calls → core/model-calls}/anthropic-model-call.d.ts +1 -1
- package/dist/{model-calls → core/model-calls}/deepseek-model-call.d.ts +1 -1
- package/dist/{model-calls → core/model-calls}/openai-model-call.d.ts +1 -1
- package/dist/core/run-agent.d.ts +182 -0
- package/dist/{run-agent.js → core/run-agent.js} +233 -51
- package/dist/{session-store.d.ts → core/session-store.d.ts} +13 -1
- package/dist/{session-store.js → core/session-store.js} +19 -0
- package/dist/core/system-tools/ask_user.d.ts +72 -0
- package/dist/core/system-tools/ask_user.js +136 -0
- package/dist/core/system-tools/index.d.ts +3 -0
- package/dist/core/system-tools/index.js +12 -0
- package/dist/core/system-tools/read_file.d.ts +28 -0
- package/dist/core/system-tools/read_file.js +64 -0
- package/dist/index.d.ts +19 -16
- package/dist/index.js +16 -9
- package/package.json +33 -25
- package/dist/cli.js +0 -191
- package/dist/discover-agents.d.ts +0 -24
- package/dist/run-agent.d.ts +0 -92
- /package/dist/{agent-config.d.ts → core/agent-config.d.ts} +0 -0
- /package/dist/{agent-config.js → core/agent-config.js} +0 -0
- /package/dist/{model-calls → core/model-calls}/anthropic-model-call.js +0 -0
- /package/dist/{model-calls → core/model-calls}/deepseek-model-call.js +0 -0
- /package/dist/{model-calls → core/model-calls}/openai-model-call.js +0 -0
- /package/dist/{vector-index.d.ts → core/vector-index.d.ts} +0 -0
- /package/dist/{vector-index.js → core/vector-index.js} +0 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// The system ask_user tool (see this folder's own index.ts, and
|
|
2
|
+
// run-agent.ts's tools merge) — lets a model pause mid-turn and ask the
|
|
3
|
+
// human operator a genuinely ambiguous question instead of guessing, the
|
|
4
|
+
// same reasoning WebApprover exists for permission 'ask' decisions
|
|
5
|
+
// (web/web-approver.ts), just generalized from a fixed allow/deny to an open
|
|
6
|
+
// answer. No companion skill the way read_file has
|
|
7
|
+
// system-skills/composio-large-outputs — that skill exists to bridge an
|
|
8
|
+
// *indirect* trigger (another tool's storedInFile result implying
|
|
9
|
+
// read_file should be called); this tool's own description is a
|
|
10
|
+
// complete, self-contained instruction with no such indirection to
|
|
11
|
+
// bridge.
|
|
12
|
+
//
|
|
13
|
+
// A fresh tool per runAgent() call, not a static export like read_file —
|
|
14
|
+
// it needs somewhere to register a pending question so a later
|
|
15
|
+
// answerQuestion() call can find and resolve it, and its onPending hook
|
|
16
|
+
// needs to close over that call's own onEvent — see createAskUserTool's
|
|
17
|
+
// own doc comment, and web/web-approver.ts's createTrackedApprover for the
|
|
18
|
+
// same reasoning applied to approvals.
|
|
19
|
+
import { randomUUID } from 'node:crypto';
|
|
20
|
+
import { createInterface } from 'node:readline/promises';
|
|
21
|
+
// One shared, global registry — unlike WebApprover's pending approvals
|
|
22
|
+
// (split across a shared instance and a fresh one per streamed turn, so
|
|
23
|
+
// each turn's onPending can target its own SSE connection), a question
|
|
24
|
+
// only ever needs *one* place to live: onPending here is just an optional
|
|
25
|
+
// extra notification layered on top, not a routing key, so every
|
|
26
|
+
// question — from any call, streamed or not — is always answerable via
|
|
27
|
+
// answerQuestion()/listQuestions() below regardless of how it was raised.
|
|
28
|
+
const questionsById = new Map();
|
|
29
|
+
const DEFAULT_TIMEOUT_MS = 5 * 60_000;
|
|
30
|
+
/** Blocking terminal prompt — the same fallback actauth's own
|
|
31
|
+
* ConsoleApprover uses for a permission ask with nowhere else to go
|
|
32
|
+
* (see createAskUserTool below for when this applies). */
|
|
33
|
+
async function promptOnConsole(question, options) {
|
|
34
|
+
console.log(`\n[ask_user] ${question}`);
|
|
35
|
+
if (options?.length)
|
|
36
|
+
console.log(` options: ${options.join(', ')}`);
|
|
37
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
38
|
+
try {
|
|
39
|
+
return (await rl.question('> ')).trim();
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
rl.close();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Builds the ask_user ToolDefinition for one runAgent() call.
|
|
46
|
+
*
|
|
47
|
+
* context identifies *whose* question this is (see PendingQuestion's own
|
|
48
|
+
* doc comment) — without it, listQuestions() would have no way to scope
|
|
49
|
+
* down to one agent/conversation, and every caller would see every
|
|
50
|
+
* pending question this whole process has ever raised.
|
|
51
|
+
*
|
|
52
|
+
* onPending, when given, is how a live caller (adapters/http.ts's
|
|
53
|
+
* streaming route passes `(q) => log('question:pending', q)`, which
|
|
54
|
+
* onEvent forwards straight onto that turn's SSE connection — see
|
|
55
|
+
* playground.ts's own inline card for it) finds out immediately instead
|
|
56
|
+
* of having to poll listQuestions(). It's a notification only, not a
|
|
57
|
+
* routing key: the question is registered in the same shared
|
|
58
|
+
* questionsById either way, so answerQuestion() works regardless of
|
|
59
|
+
* whether anything was listening for onPending in the first place.
|
|
60
|
+
*
|
|
61
|
+
* Omitted entirely (a plain CLI run, or any caller with no live channel
|
|
62
|
+
* of its own) falls back to a blocking terminal prompt instead of a
|
|
63
|
+
* pending entry nothing could ever answer — same reasoning
|
|
64
|
+
* ConsoleApprover exists for permission asks with nowhere else to go.
|
|
65
|
+
*
|
|
66
|
+
* Named `system_ask_user`, not the bare `ask_user` an agent author would
|
|
67
|
+
* naturally reach for on their own — see system-tools/read_file.ts's own
|
|
68
|
+
* doc comment on the same `system_` prefix for the full reasoning
|
|
69
|
+
* (unlikely to collide by accident, and run-agent.ts's own
|
|
70
|
+
* systemToolInstances gate bypass treats a same-named override as a
|
|
71
|
+
* deliberate opt-out when one does happen). It matters even more here
|
|
72
|
+
* than for read_file: this tool *is* a human's only way to answer the
|
|
73
|
+
* agent in the first place, so it can never be gated at all without a
|
|
74
|
+
* real deadlock, not just a redundant check — a name any other tool
|
|
75
|
+
* could plausibly reach for by accident would risk silently losing that
|
|
76
|
+
* guarantee, not just risk a surprising override. */
|
|
77
|
+
export function createAskUserTool(context, onPending) {
|
|
78
|
+
return {
|
|
79
|
+
name: 'system_ask_user',
|
|
80
|
+
description: "Ask the human operator a clarifying question before proceeding, when the request is genuinely ambiguous and guessing risks doing the wrong thing. Don't use this for things you can reasonably infer or look up yourself — only for real ambiguity a human actually needs to resolve. Optionally suggest a fixed set of answers; the human can still respond with free text regardless. Blocks until answered (or timed out).",
|
|
81
|
+
input_schema: {
|
|
82
|
+
type: 'object',
|
|
83
|
+
properties: {
|
|
84
|
+
question: { type: 'string', description: 'The question to ask, in plain language.' },
|
|
85
|
+
options: { type: 'array', items: { type: 'string' }, description: 'Optional suggested answers to offer alongside free text.' },
|
|
86
|
+
},
|
|
87
|
+
required: ['question'],
|
|
88
|
+
},
|
|
89
|
+
execute: async (input) => {
|
|
90
|
+
const question = String(input.question);
|
|
91
|
+
const options = Array.isArray(input.options) ? input.options.map(String) : undefined;
|
|
92
|
+
if (!onPending)
|
|
93
|
+
return promptOnConsole(question, options);
|
|
94
|
+
const id = randomUUID();
|
|
95
|
+
const entry = { id, question, options, agent: context.agent, sessionId: context.sessionId, requestedAt: new Date().toISOString() };
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
const timer = setTimeout(() => {
|
|
98
|
+
questionsById.delete(id);
|
|
99
|
+
resolve('(no answer — timed out)');
|
|
100
|
+
}, DEFAULT_TIMEOUT_MS);
|
|
101
|
+
questionsById.set(id, { entry, resolve, timer });
|
|
102
|
+
onPending(entry);
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Oldest first, same ordering reasoning web/web-approver.ts's listApprovals
|
|
108
|
+
* uses — whichever question has been waiting longest surfaces first.
|
|
109
|
+
* Unfiltered (both omitted) returns every question pending anywhere in
|
|
110
|
+
* this process — real uses (an operator's own admin view, say) should
|
|
111
|
+
* filter, since that's every conversation's questions mixed together. */
|
|
112
|
+
export function listQuestions(filter) {
|
|
113
|
+
return [...questionsById.values()]
|
|
114
|
+
.map((p) => p.entry)
|
|
115
|
+
.filter((q) => (!filter?.agent || q.agent === filter.agent) && (!filter?.sessionId || q.sessionId === filter.sessionId))
|
|
116
|
+
.sort((a, b) => a.requestedAt.localeCompare(b.requestedAt));
|
|
117
|
+
}
|
|
118
|
+
/** Peek at a pending question's own entry (its agent/sessionId, in
|
|
119
|
+
* particular) without answering it — adapters/http.ts needs this *before*
|
|
120
|
+
* calling answerQuestion() below, to know which turn's early-return race
|
|
121
|
+
* (see its own sessionTurns) to resume once this one's answered. */
|
|
122
|
+
export function findQuestion(id) {
|
|
123
|
+
return questionsById.get(id)?.entry;
|
|
124
|
+
}
|
|
125
|
+
/** Returns false for an unknown/already-answered/timed-out id, so the
|
|
126
|
+
* caller can report that distinctly (a 404) instead of silently
|
|
127
|
+
* no-opping — same convention web/web-approver.ts's decideApproval uses. */
|
|
128
|
+
export function answerQuestion(id, answer) {
|
|
129
|
+
const pending = questionsById.get(id);
|
|
130
|
+
if (!pending)
|
|
131
|
+
return false;
|
|
132
|
+
clearTimeout(pending.timer);
|
|
133
|
+
questionsById.delete(id);
|
|
134
|
+
pending.resolve(answer);
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Aggregates every system tool into one place, the same one-file-per-tool
|
|
2
|
+
// + index.ts convention agents/file-agent/tools/index.ts already follows.
|
|
3
|
+
// systemTools is the *static* set (fixed closures, no per-call state
|
|
4
|
+
// needed) that run-agent.ts spreads directly into every agent's tools —
|
|
5
|
+
// see its own doc comment there for the merge/precedence rules. Not every
|
|
6
|
+
// system tool fits that shape, though: ask_user needs a fresh instance
|
|
7
|
+
// per runAgent() call (its onPending hook closes over that call's own
|
|
8
|
+
// onEvent — see ask_user.ts's own doc comment), so its *builder* is
|
|
9
|
+
// re-exported here instead of an instance.
|
|
10
|
+
import { readFile } from './read_file.js';
|
|
11
|
+
export const systemTools = [readFile];
|
|
12
|
+
export { createAskUserTool, listQuestions, answerQuestion, findQuestion } from './ask_user.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../agent-config.js';
|
|
2
|
+
/** A path-restricted read_file, auto-available to every agent (see
|
|
3
|
+
* run-agent.ts's own dedupeToolsByName + tools merge) — narrowly scoped
|
|
4
|
+
* to the OS temp directory, which is where a gateway tool's real result
|
|
5
|
+
* lands when a call returns `storedInFile: true` instead of the data
|
|
6
|
+
* itself (confirmed live against the real composio CLI: `composio
|
|
7
|
+
* execute`'s own outputFilePath always lands under os.tmpdir()).
|
|
8
|
+
*
|
|
9
|
+
* Deliberately *not* the same unscoped tool agents/file-agent/tools/
|
|
10
|
+
* read_file.ts already has (arbitrary path, no restriction at all,
|
|
11
|
+
* predates this): making arbitrary-path file reads available to *every*
|
|
12
|
+
* agent by default — including ones wired to less-trusted gateway tools
|
|
13
|
+
* or end-user-facing flows — would be a real, unbounded new capability,
|
|
14
|
+
* not just a convenience. Resolving symlinks (realpathSync) on both the
|
|
15
|
+
* allowed root and the requested path closes the obvious bypass (a
|
|
16
|
+
* symlink inside the temp dir pointing outside it).
|
|
17
|
+
*
|
|
18
|
+
* Named `system_read_file`, not the bare `read_file` an agent author
|
|
19
|
+
* would naturally reach for on their own (file-agent's own tool included)
|
|
20
|
+
* — a `system_`-prefixed name is unlikely enough to collide by accident
|
|
21
|
+
* that dedupeToolsByName's "an agent's own tool of the same name always
|
|
22
|
+
* wins" rule stays a deliberate, rare opt-out rather than something a
|
|
23
|
+
* completely unrelated tool name silently triggers. It also matters for
|
|
24
|
+
* run-agent.ts's own systemToolInstances gate bypass (see there): once an
|
|
25
|
+
* agent's own tool *does* collide on this exact name, gating goes back to
|
|
26
|
+
* normal for it, on purpose — a name this specific is what makes that
|
|
27
|
+
* an intentional choice on the agent author's part, not an accident. */
|
|
28
|
+
export declare const readFile: ToolDefinition;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Tools every agent gets automatically (see run-agent.ts's own tools
|
|
2
|
+
// merge, and this folder's own index.ts) — not something an agent opts
|
|
3
|
+
// into, the way its own agents/<name>/tools/ or a gateway-tools.yml
|
|
4
|
+
// source is. Kept to the one thing that's genuinely required
|
|
5
|
+
// infrastructure, not a convenience: a gateway tool (Composio's,
|
|
6
|
+
// confirmed live) that reports `storedInFile: true` instead of returning
|
|
7
|
+
// its real output inline is otherwise a dead end for an agent that never
|
|
8
|
+
// happened to define its own file-reading tool.
|
|
9
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { resolve, sep } from 'node:path';
|
|
12
|
+
// Resolved once, at import time — os.tmpdir() itself doesn't change at
|
|
13
|
+
// runtime, and resolving it through realpathSync up front (not per call)
|
|
14
|
+
// means a symlink in the OS temp path itself (e.g. macOS's /tmp ->
|
|
15
|
+
// /private/tmp) is accounted for once, not re-resolved on every call.
|
|
16
|
+
const ALLOWED_ROOT = realpathSync(tmpdir());
|
|
17
|
+
function isWithinAllowedRoot(path) {
|
|
18
|
+
return path === ALLOWED_ROOT || path.startsWith(ALLOWED_ROOT + sep);
|
|
19
|
+
}
|
|
20
|
+
/** A path-restricted read_file, auto-available to every agent (see
|
|
21
|
+
* run-agent.ts's own dedupeToolsByName + tools merge) — narrowly scoped
|
|
22
|
+
* to the OS temp directory, which is where a gateway tool's real result
|
|
23
|
+
* lands when a call returns `storedInFile: true` instead of the data
|
|
24
|
+
* itself (confirmed live against the real composio CLI: `composio
|
|
25
|
+
* execute`'s own outputFilePath always lands under os.tmpdir()).
|
|
26
|
+
*
|
|
27
|
+
* Deliberately *not* the same unscoped tool agents/file-agent/tools/
|
|
28
|
+
* read_file.ts already has (arbitrary path, no restriction at all,
|
|
29
|
+
* predates this): making arbitrary-path file reads available to *every*
|
|
30
|
+
* agent by default — including ones wired to less-trusted gateway tools
|
|
31
|
+
* or end-user-facing flows — would be a real, unbounded new capability,
|
|
32
|
+
* not just a convenience. Resolving symlinks (realpathSync) on both the
|
|
33
|
+
* allowed root and the requested path closes the obvious bypass (a
|
|
34
|
+
* symlink inside the temp dir pointing outside it).
|
|
35
|
+
*
|
|
36
|
+
* Named `system_read_file`, not the bare `read_file` an agent author
|
|
37
|
+
* would naturally reach for on their own (file-agent's own tool included)
|
|
38
|
+
* — a `system_`-prefixed name is unlikely enough to collide by accident
|
|
39
|
+
* that dedupeToolsByName's "an agent's own tool of the same name always
|
|
40
|
+
* wins" rule stays a deliberate, rare opt-out rather than something a
|
|
41
|
+
* completely unrelated tool name silently triggers. It also matters for
|
|
42
|
+
* run-agent.ts's own systemToolInstances gate bypass (see there): once an
|
|
43
|
+
* agent's own tool *does* collide on this exact name, gating goes back to
|
|
44
|
+
* normal for it, on purpose — a name this specific is what makes that
|
|
45
|
+
* an intentional choice on the agent author's part, not an accident. */
|
|
46
|
+
export const readFile = {
|
|
47
|
+
name: 'system_read_file',
|
|
48
|
+
description: "Read a text file from the OS temp directory — this is where a gateway tool's real output lands when its result says storedInFile: true instead of returning the data directly (see the outputFilePath it gives you). Not for reading arbitrary project or system files.",
|
|
49
|
+
input_schema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
|
|
50
|
+
execute: async (input) => {
|
|
51
|
+
const requested = resolve(String(input.path));
|
|
52
|
+
let real;
|
|
53
|
+
try {
|
|
54
|
+
real = realpathSync(requested);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
throw new Error(`No such file: ${requested}`);
|
|
58
|
+
}
|
|
59
|
+
if (!isWithinAllowedRoot(real)) {
|
|
60
|
+
throw new Error(`system_read_file can only read files under the OS temp directory (${ALLOWED_ROOT}) — got ${real}.`);
|
|
61
|
+
}
|
|
62
|
+
return readFileSync(real, 'utf8');
|
|
63
|
+
},
|
|
64
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
|
-
export { runAgent } from '#run-agent.js';
|
|
2
|
-
export type { Message, ModelCall, ModelContentBlock, ModelResponse, RunAgentOptions, RunAgentResult, } from '#run-agent.js';
|
|
3
|
-
export type { AgentConfig, AgentModelConfig, ToolSchema, ToolDefinition } from '#agent-config.js';
|
|
4
|
-
export
|
|
5
|
-
export
|
|
6
|
-
export {
|
|
7
|
-
export
|
|
8
|
-
export {
|
|
9
|
-
export
|
|
10
|
-
export {
|
|
11
|
-
export
|
|
12
|
-
export {
|
|
13
|
-
export
|
|
14
|
-
export {
|
|
15
|
-
export
|
|
16
|
-
export {
|
|
1
|
+
export { runAgent } from '#core/run-agent.js';
|
|
2
|
+
export type { Message, ModelCall, ModelContentBlock, ModelResponse, RunAgentOptions, RunAgentResult, } from '#core/run-agent.js';
|
|
3
|
+
export type { AgentConfig, AgentModelConfig, ToolSchema, ToolDefinition } from '#core/agent-config.js';
|
|
4
|
+
export type * from './core/loop-events.js';
|
|
5
|
+
export { streamMessage, streamMessageWithCallbacks, sendMessage, approveCall, denyCall, answerQuestion, getSessionHistory, } from './core/client.js';
|
|
6
|
+
export type { RequestOptions, SendMessageResult, PendingApprovalResult, PendingQuestionResult, PendingResult, MessageResult, StreamMessageCallbacks, } from './core/client.js';
|
|
7
|
+
export { FileSessionStore, RedisSessionStore, createSessionStore } from './core/session-store.js';
|
|
8
|
+
export type { SessionStore, SessionResult, RedisSessionStoreOptions } from './core/session-store.js';
|
|
9
|
+
export { VectorIndex, embed, cosineSimilarity } from './core/vector-index.js';
|
|
10
|
+
export type { Document, ScoredDocument } from './core/vector-index.js';
|
|
11
|
+
export { createAnthropicModelCall } from './core/model-calls/anthropic-model-call.js';
|
|
12
|
+
export type { AnthropicModelCallOptions } from './core/model-calls/anthropic-model-call.js';
|
|
13
|
+
export { createOpenAIModelCall } from './core/model-calls/openai-model-call.js';
|
|
14
|
+
export type { OpenAIModelCallOptions } from './core/model-calls/openai-model-call.js';
|
|
15
|
+
export { createDeepSeekModelCall } from './core/model-calls/deepseek-model-call.js';
|
|
16
|
+
export type { DeepSeekModelCallOptions } from './core/model-calls/deepseek-model-call.js';
|
|
17
|
+
export { discoverAgents, loadAgentModule } from './core/discover-agents.js';
|
|
18
|
+
export type { AgentModule } from './core/discover-agents.js';
|
|
19
|
+
export { agentAsTool } from './core/agent-as-tool.js';
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
// The public API — everything a consumer building on loopengine needs to
|
|
2
|
-
// define and run their own agent. `agents/*.ts`, `agent-registry.ts`,
|
|
2
|
+
// define and run their own agent. `agents/*.ts`, `core/agent-registry.ts`,
|
|
3
3
|
// `adapters/*.ts`, and the Dockerfile are this repo's own reference app
|
|
4
4
|
// built on top of this surface, not part of it — see the README section
|
|
5
5
|
// "Using loopengine as a library" for the boundary and why.
|
|
6
|
-
export { runAgent } from '#run-agent.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export {
|
|
6
|
+
export { runAgent } from '#core/run-agent.js';
|
|
7
|
+
// The framework-agnostic browser client for adapters/http.ts's own wire
|
|
8
|
+
// protocol (see client.ts's own header comment) — this, not a deep
|
|
9
|
+
// subpath import, is how a consumer's React/Vue/etc. chat UI is meant to
|
|
10
|
+
// reach it: this package has no "exports" map, so every other module here
|
|
11
|
+
// is re-exported through this single entry point too, and client.ts is no
|
|
12
|
+
// exception.
|
|
13
|
+
export { streamMessage, streamMessageWithCallbacks, sendMessage, approveCall, denyCall, answerQuestion, getSessionHistory, } from './core/client.js';
|
|
14
|
+
export { FileSessionStore, RedisSessionStore, createSessionStore } from './core/session-store.js';
|
|
15
|
+
export { VectorIndex, embed, cosineSimilarity } from './core/vector-index.js';
|
|
16
|
+
export { createAnthropicModelCall } from './core/model-calls/anthropic-model-call.js';
|
|
17
|
+
export { createOpenAIModelCall } from './core/model-calls/openai-model-call.js';
|
|
18
|
+
export { createDeepSeekModelCall } from './core/model-calls/deepseek-model-call.js';
|
|
19
|
+
export { discoverAgents, loadAgentModule } from './core/discover-agents.js';
|
|
20
|
+
export { agentAsTool } from './core/agent-as-tool.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loopengine",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.20",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Runtime for tool-using AI agents with a transparent ReAct loop.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"main": "dist/index.js",
|
|
13
13
|
"types": "dist/index.d.ts",
|
|
14
14
|
"bin": {
|
|
15
|
-
"loopengine": "dist/cli.js"
|
|
15
|
+
"loopengine": "dist/bin/cli.js"
|
|
16
16
|
},
|
|
17
17
|
"imports": {
|
|
18
18
|
"#*": "./*"
|
|
@@ -20,37 +20,45 @@
|
|
|
20
20
|
"files": [
|
|
21
21
|
"dist/index.js",
|
|
22
22
|
"dist/index.d.ts",
|
|
23
|
-
"dist/cli.js",
|
|
24
|
-
"dist/cli.d.ts",
|
|
25
|
-
"dist/run-agent.js",
|
|
26
|
-
"dist/run-agent.d.ts",
|
|
27
|
-
"dist/
|
|
28
|
-
"dist/
|
|
29
|
-
"dist/
|
|
30
|
-
"dist/
|
|
31
|
-
"dist/
|
|
32
|
-
"dist/
|
|
33
|
-
"dist/
|
|
34
|
-
"dist/
|
|
35
|
-
"dist/
|
|
36
|
-
"dist/
|
|
37
|
-
"dist/model-calls/
|
|
38
|
-
"dist/model-calls/
|
|
39
|
-
"dist/
|
|
40
|
-
"dist/
|
|
41
|
-
"dist/
|
|
42
|
-
"dist/
|
|
23
|
+
"dist/bin/cli.js",
|
|
24
|
+
"dist/bin/cli.d.ts",
|
|
25
|
+
"dist/core/run-agent.js",
|
|
26
|
+
"dist/core/run-agent.d.ts",
|
|
27
|
+
"dist/core/loop-events.js",
|
|
28
|
+
"dist/core/loop-events.d.ts",
|
|
29
|
+
"dist/core/client.js",
|
|
30
|
+
"dist/core/client.d.ts",
|
|
31
|
+
"dist/core/agent-config.js",
|
|
32
|
+
"dist/core/agent-config.d.ts",
|
|
33
|
+
"dist/core/session-store.js",
|
|
34
|
+
"dist/core/session-store.d.ts",
|
|
35
|
+
"dist/core/vector-index.js",
|
|
36
|
+
"dist/core/vector-index.d.ts",
|
|
37
|
+
"dist/core/model-calls/anthropic-model-call.js",
|
|
38
|
+
"dist/core/model-calls/anthropic-model-call.d.ts",
|
|
39
|
+
"dist/core/model-calls/openai-model-call.js",
|
|
40
|
+
"dist/core/model-calls/openai-model-call.d.ts",
|
|
41
|
+
"dist/core/model-calls/deepseek-model-call.js",
|
|
42
|
+
"dist/core/model-calls/deepseek-model-call.d.ts",
|
|
43
|
+
"dist/core/discover-agents.js",
|
|
44
|
+
"dist/core/discover-agents.d.ts",
|
|
45
|
+
"dist/core/agent-as-tool.js",
|
|
46
|
+
"dist/core/agent-as-tool.d.ts",
|
|
47
|
+
"dist/core/gateway-tools.js",
|
|
48
|
+
"dist/core/gateway-tools.d.ts",
|
|
49
|
+
"dist/core/system-tools",
|
|
50
|
+
"core/system-skills",
|
|
43
51
|
"LICENSE"
|
|
44
52
|
],
|
|
45
53
|
"scripts": {
|
|
46
|
-
"build": "tsc
|
|
54
|
+
"build": "tsc; s=$?; chmod +x dist/bin/cli.js; exit $s",
|
|
47
55
|
"prepublishOnly": "npm run build",
|
|
48
|
-
"start": "node dist/adapters/http.js",
|
|
56
|
+
"start": "node --env-file-if-exists=.env dist/adapters/http.js",
|
|
49
57
|
"test": "vitest run"
|
|
50
58
|
},
|
|
51
59
|
"dependencies": {
|
|
52
60
|
"@anthropic-ai/sdk": "^0.116.0",
|
|
53
|
-
"actauth": "^0.0.
|
|
61
|
+
"actauth": "^0.0.7",
|
|
54
62
|
"contextclip": "^0.0.1",
|
|
55
63
|
"ioredis": "^6.0.0",
|
|
56
64
|
"mcpplug": "^0.0.1",
|
package/dist/cli.js
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// The `loopengine` bin — distinct from adapters/cli.ts, which runs an
|
|
3
|
-
// agent, not scaffolds one. Generates the agents/<name>/index.ts
|
|
4
|
-
// boilerplate the folder convention expects (see agent-config.ts's own
|
|
5
|
-
// doc comments, or the README's "Define your first agent"), so using
|
|
6
|
-
// that convention doesn't mean memorizing its shape and hand-writing it
|
|
7
|
-
// every time.
|
|
8
|
-
import { realpathSync } from 'node:fs';
|
|
9
|
-
import { access, mkdir, writeFile } from 'node:fs/promises';
|
|
10
|
-
import path from 'node:path';
|
|
11
|
-
import { fileURLToPath } from 'node:url';
|
|
12
|
-
const NAME_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
13
|
-
export class AgentNameError extends Error {
|
|
14
|
-
}
|
|
15
|
-
export class AgentExistsError extends Error {
|
|
16
|
-
}
|
|
17
|
-
export class AgentNotFoundError extends Error {
|
|
18
|
-
}
|
|
19
|
-
export function agentIndexTemplate(name) {
|
|
20
|
-
return `import type { AgentConfig } from 'loopengine'
|
|
21
|
-
|
|
22
|
-
export const config: AgentConfig = {
|
|
23
|
-
name: '${name}',
|
|
24
|
-
systemPrompt: 'You are ...',
|
|
25
|
-
model: { provider: 'anthropic', model: 'claude-sonnet-5' }, // reads ANTHROPIC_API_KEY
|
|
26
|
-
}
|
|
27
|
-
`;
|
|
28
|
-
}
|
|
29
|
-
/** Same shape as agentIndexTemplate, plus `toolDescription` — the one
|
|
30
|
-
* field a subagent needs that a top-level agent doesn't. run-agent.ts's
|
|
31
|
-
* loadSubagentTools wraps this config with agentAsTool, which throws
|
|
32
|
-
* without a real toolDescription: it's what the parent agent's model
|
|
33
|
-
* reads to decide when to delegate here, and systemPrompt (instructions
|
|
34
|
-
* for this agent itself) isn't a substitute. See agent-as-tool.ts. */
|
|
35
|
-
export function subagentIndexTemplate(name) {
|
|
36
|
-
return `import type { AgentConfig } from 'loopengine'
|
|
37
|
-
|
|
38
|
-
export const config: AgentConfig = {
|
|
39
|
-
name: '${name}',
|
|
40
|
-
systemPrompt: 'You are ...',
|
|
41
|
-
toolDescription: 'Call this when ...',
|
|
42
|
-
model: { provider: 'anthropic', model: 'claude-sonnet-5' }, // reads ANTHROPIC_API_KEY
|
|
43
|
-
}
|
|
44
|
-
`;
|
|
45
|
-
}
|
|
46
|
-
/** Writes agents/<name>/index.ts under baseDir and returns its path.
|
|
47
|
-
* Only that one file — tools, rules, and skillsDirs are left to their
|
|
48
|
-
* smart defaults (see AgentConfig's own doc comments), the same as the
|
|
49
|
-
* README's own "simplest possible agent" example. */
|
|
50
|
-
export async function scaffoldAgent(baseDir, name) {
|
|
51
|
-
if (!NAME_PATTERN.test(name)) {
|
|
52
|
-
throw new AgentNameError(`Agent name must be lowercase, alphanumeric, hyphen-separated (e.g. "weather-agent") — got "${name}"`);
|
|
53
|
-
}
|
|
54
|
-
const dir = path.join(baseDir, 'agents', name);
|
|
55
|
-
const indexPath = path.join(dir, 'index.ts');
|
|
56
|
-
if (await pathExists(indexPath)) {
|
|
57
|
-
throw new AgentExistsError(`agents/${name}/index.ts already exists — pick a different name or edit it directly.`);
|
|
58
|
-
}
|
|
59
|
-
await mkdir(dir, { recursive: true });
|
|
60
|
-
await writeFile(indexPath, agentIndexTemplate(name));
|
|
61
|
-
return indexPath;
|
|
62
|
-
}
|
|
63
|
-
/** Resolves `parent` — a single agent name, or a `/`-joined path of
|
|
64
|
-
* names for nesting more than one level deep (e.g.
|
|
65
|
-
* "support-orchestrator/billing-agent" to scaffold a subagent under an
|
|
66
|
-
* *existing subagent*) — to the real directory that agent's index.ts
|
|
67
|
-
* lives in: agents/<seg0>/subagents/<seg1>/subagents/<seg2>/... Every
|
|
68
|
-
* segment gets the same lowercase-hyphen validation a plain agent name
|
|
69
|
-
* does; there's no separate "path" naming rule. */
|
|
70
|
-
function resolveParentDir(baseDir, parent) {
|
|
71
|
-
const segments = parent.split('/');
|
|
72
|
-
for (const segment of segments) {
|
|
73
|
-
if (!NAME_PATTERN.test(segment)) {
|
|
74
|
-
throw new AgentNameError(`Parent must be a lowercase, alphanumeric, hyphen-separated agent name, or several joined by '/' for nesting (e.g. "support-orchestrator/billing-agent") — got "${parent}"`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return segments.slice(1).reduce((dir, segment) => path.join(dir, 'subagents', segment), path.join(baseDir, 'agents', segments[0]));
|
|
78
|
-
}
|
|
79
|
-
/** Writes <parentDir>/subagents/<name>/index.ts under baseDir and
|
|
80
|
-
* returns its path, where <parentDir> is `parent` resolved by
|
|
81
|
-
* resolveParentDir — the folder convention run-agent.ts's
|
|
82
|
-
* loadSubagentTools auto-discovers, so this subagent becomes one of
|
|
83
|
-
* `parent`'s tools with no further edits: no import, no
|
|
84
|
-
* AgentConfig.tools entry, nothing to register by hand. Requires
|
|
85
|
-
* `parent` to already exist (its own index.ts) — a subagent without a
|
|
86
|
-
* parent to nest under is just a regular top-level agent, which
|
|
87
|
-
* scaffoldAgent already covers. Nesting more than one level is just
|
|
88
|
-
* `parent` itself being a path — see resolveParentDir. */
|
|
89
|
-
export async function scaffoldSubagent(baseDir, parent, name) {
|
|
90
|
-
const parentDir = resolveParentDir(baseDir, parent);
|
|
91
|
-
if (!NAME_PATTERN.test(name)) {
|
|
92
|
-
throw new AgentNameError(`Subagent name must be lowercase, alphanumeric, hyphen-separated (e.g. "billing-agent") — got "${name}"`);
|
|
93
|
-
}
|
|
94
|
-
const parentIndexPath = path.join(parentDir, 'index.ts');
|
|
95
|
-
if (!(await pathExists(parentIndexPath))) {
|
|
96
|
-
throw new AgentNotFoundError(`'${parent}' doesn't exist yet (expected ${parentIndexPath}) — create it first.`);
|
|
97
|
-
}
|
|
98
|
-
const dir = path.join(parentDir, 'subagents', name);
|
|
99
|
-
const indexPath = path.join(dir, 'index.ts');
|
|
100
|
-
if (await pathExists(indexPath)) {
|
|
101
|
-
throw new AgentExistsError(`A subagent named '${name}' already exists under '${parent}' (${indexPath}) — pick a different name or edit it directly.`);
|
|
102
|
-
}
|
|
103
|
-
await mkdir(dir, { recursive: true });
|
|
104
|
-
await writeFile(indexPath, subagentIndexTemplate(name));
|
|
105
|
-
return indexPath;
|
|
106
|
-
}
|
|
107
|
-
async function pathExists(p) {
|
|
108
|
-
try {
|
|
109
|
-
await access(p);
|
|
110
|
-
return true;
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
async function main() {
|
|
117
|
-
const [, , command, ...rest] = process.argv;
|
|
118
|
-
if (command === 'add-agent') {
|
|
119
|
-
const [name] = rest;
|
|
120
|
-
if (!name) {
|
|
121
|
-
console.error('Usage: loopengine add-agent <name>');
|
|
122
|
-
process.exitCode = 1;
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
try {
|
|
126
|
-
await scaffoldAgent(process.cwd(), name);
|
|
127
|
-
}
|
|
128
|
-
catch (err) {
|
|
129
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
130
|
-
process.exitCode = 1;
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
console.log(`Created agents/${name}/index.ts`);
|
|
134
|
-
console.log();
|
|
135
|
-
console.log('tools, rules, and skillsDirs are all omitted — they default to:');
|
|
136
|
-
console.log(` agents/${name}/tools/index.ts (no tools until you add one)`);
|
|
137
|
-
console.log(` agents/${name}/actauth.yml (denies every tool until you add one)`);
|
|
138
|
-
console.log(` agents/${name}/skills/ (no skills until you add one)`);
|
|
139
|
-
console.log();
|
|
140
|
-
console.log(`Run it: npx tsx adapters/cli.ts --agent ${name} "hello"`);
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
if (command === 'add-subagent') {
|
|
144
|
-
const [parent, name] = rest;
|
|
145
|
-
if (!parent || !name) {
|
|
146
|
-
console.error('Usage: loopengine add-subagent <parent> <name>');
|
|
147
|
-
console.error(' <parent> can be a single agent name, or several joined by \'/\' to nest deeper (e.g. "support-orchestrator/billing-agent")');
|
|
148
|
-
process.exitCode = 1;
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
let indexPath;
|
|
152
|
-
try {
|
|
153
|
-
indexPath = await scaffoldSubagent(process.cwd(), parent, name);
|
|
154
|
-
}
|
|
155
|
-
catch (err) {
|
|
156
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
157
|
-
process.exitCode = 1;
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
console.log(`Created ${path.relative(process.cwd(), indexPath)}`);
|
|
161
|
-
console.log();
|
|
162
|
-
console.log(`Auto-registered as a tool on '${parent}' — no import, no tools array edit needed.`);
|
|
163
|
-
console.log(`Fill in toolDescription: it's what '${parent}'s model reads to decide when to call '${name}'.`);
|
|
164
|
-
console.log();
|
|
165
|
-
console.log(`Run the top-level agent: npx tsx adapters/cli.ts --agent ${parent.split('/')[0]} "hello"`);
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
console.error('Usage: loopengine add-agent <name>');
|
|
169
|
-
console.error(' loopengine add-subagent <parent> <name>');
|
|
170
|
-
process.exitCode = 1;
|
|
171
|
-
}
|
|
172
|
-
// A package-manager shim (npx, node_modules/.bin) invokes this file
|
|
173
|
-
// through a symlink, not its real path — process.argv[1] is the
|
|
174
|
-
// symlink's path, while Node resolves import.meta.url to the real path
|
|
175
|
-
// when loading an ES module. Comparing the raw argv[1] against the
|
|
176
|
-
// resolved module URL fails in exactly that case and main() silently
|
|
177
|
-
// never runs — realpath argv[1] first so both sides are resolved. (Same
|
|
178
|
-
// bug, same fix, as create-loopengine's own cli.ts — see its git history
|
|
179
|
-
// for the live symlink repro, and skillgarden's CLI before that.)
|
|
180
|
-
function isMain() {
|
|
181
|
-
if (!process.argv[1])
|
|
182
|
-
return false;
|
|
183
|
-
try {
|
|
184
|
-
return realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
185
|
-
}
|
|
186
|
-
catch {
|
|
187
|
-
return false;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
if (isMain())
|
|
191
|
-
main();
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { AgentConfig } from '#agent-config.js';
|
|
2
|
-
import type { ModelCall } from '#run-agent.js';
|
|
3
|
-
export interface AgentModule {
|
|
4
|
-
config: AgentConfig;
|
|
5
|
-
/** Factory, not a shared instance — a fresh ModelCall per request/session. */
|
|
6
|
-
createModelCall: () => ModelCall;
|
|
7
|
-
}
|
|
8
|
-
/** Imports one agent module at `path` and resolves it to an AgentModule —
|
|
9
|
-
* the single-file version of what discoverAgents does over a whole
|
|
10
|
-
* directory, factored out so run-agent.ts's subagent loader (see
|
|
11
|
-
* agent-as-tool.ts / loadSubagentTools) can load one `subagents/<name>/
|
|
12
|
-
* index.ts` at a time without duplicating this resolution logic.
|
|
13
|
-
* `label` is only used in error messages — discoverAgents passes
|
|
14
|
-
* `'<file> in <dir>'`, callers loading a single known path can just pass
|
|
15
|
-
* that path. */
|
|
16
|
-
export declare function loadAgentModule(path: string, label: string): Promise<AgentModule>;
|
|
17
|
-
/** Scans `dir` for agent modules — direct `.ts`/`.js` files, or
|
|
18
|
-
* subdirectories with an `index.ts`/`index.js` (see resolveModulePath) —
|
|
19
|
-
* each expected to export `config`, and either its own `createModelCall`
|
|
20
|
-
* or a `config.model` for one to be synthesized from (see
|
|
21
|
-
* AgentModelConfig's own doc comment). Throws, rather than skipping, on a
|
|
22
|
-
* module that doesn't match that shape or a duplicate AgentConfig.name —
|
|
23
|
-
* both are almost always a bug, not something to silently ignore. */
|
|
24
|
-
export declare function discoverAgents(dir: string): Promise<Map<string, AgentModule>>;
|