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,113 @@
|
|
|
1
|
+
import type { LoopEvent } from './loop-events.js';
|
|
2
|
+
import type { Message } from './run-agent.js';
|
|
3
|
+
export interface RequestOptions {
|
|
4
|
+
/** Resumes an existing conversation — omit for a fresh one; see
|
|
5
|
+
* adapters/http.ts's own sessionIdFor for how the server derives one
|
|
6
|
+
* when omitted. */
|
|
7
|
+
sessionId?: string;
|
|
8
|
+
/** Extra headers on top of 'content-type: application/json' — e.g. HTTP
|
|
9
|
+
* Basic Auth (LOOPENGINE_ADMIN_AUTH) or an AgentConfig.tenantFor header. */
|
|
10
|
+
headers?: Record<string, string>;
|
|
11
|
+
/** Merged into the request body alongside `message`/`sessionId` — for an
|
|
12
|
+
* AgentConfig with its own sessionIdFor/tenantFor reading extra fields
|
|
13
|
+
* (see adapters/http.ts's own resolveTenant). */
|
|
14
|
+
body?: Record<string, unknown>;
|
|
15
|
+
signal?: AbortSignal;
|
|
16
|
+
}
|
|
17
|
+
/** POST /agents/:name/messages's own 200 response — the turn finished,
|
|
18
|
+
* genuinely or via a synthetic stopReason (see loop-events.ts's own
|
|
19
|
+
* DoneEvent). `events` is every LoopEvent this turn produced, start to
|
|
20
|
+
* finish — the same array a streamed turn delivers frame-by-frame. */
|
|
21
|
+
export interface SendMessageResult {
|
|
22
|
+
text: string;
|
|
23
|
+
sessionId: string;
|
|
24
|
+
events: LoopEvent[];
|
|
25
|
+
stopReason?: 'max_turns' | 'denied';
|
|
26
|
+
}
|
|
27
|
+
interface PendingResultBase {
|
|
28
|
+
pending: true;
|
|
29
|
+
id: string;
|
|
30
|
+
sessionId: string;
|
|
31
|
+
events: LoopEvent[];
|
|
32
|
+
/** GET this to rehydrate the conversation (and re-discover this same
|
|
33
|
+
* pending item) if the caller navigates away before deciding it. */
|
|
34
|
+
statusUrl: string;
|
|
35
|
+
}
|
|
36
|
+
/** POST /agents/:name/messages's own 202 response when the turn needs a
|
|
37
|
+
* human 'ask' decision before it can continue — resolve it with
|
|
38
|
+
* approveCall/denyCall below, both of which return this same
|
|
39
|
+
* MessageResult union, so a caller can treat approve/deny/answer and the
|
|
40
|
+
* original send as one uniform decide-or-finish loop. */
|
|
41
|
+
export interface PendingApprovalResult extends PendingResultBase {
|
|
42
|
+
type: 'approval';
|
|
43
|
+
tool: string;
|
|
44
|
+
args: Record<string, unknown>;
|
|
45
|
+
scope: {
|
|
46
|
+
tenant: string;
|
|
47
|
+
environment: string;
|
|
48
|
+
agent: string;
|
|
49
|
+
};
|
|
50
|
+
reason: string;
|
|
51
|
+
approveUrl: string;
|
|
52
|
+
denyUrl: string;
|
|
53
|
+
}
|
|
54
|
+
/** Same as PendingApprovalResult, for the system ask_user tool's own
|
|
55
|
+
* open-ended question instead of a fixed allow/deny — resolve with
|
|
56
|
+
* answerQuestion below. */
|
|
57
|
+
export interface PendingQuestionResult extends PendingResultBase {
|
|
58
|
+
type: 'question';
|
|
59
|
+
question: string;
|
|
60
|
+
options?: string[];
|
|
61
|
+
answerUrl: string;
|
|
62
|
+
}
|
|
63
|
+
export type PendingResult = PendingApprovalResult | PendingQuestionResult;
|
|
64
|
+
export type MessageResult = SendMessageResult | PendingResult;
|
|
65
|
+
/** The non-streaming half of the protocol — one request, one response,
|
|
66
|
+
* either done (SendMessageResult) or needing a decision (PendingResult).
|
|
67
|
+
* Reach for this over streamMessage() when the caller doesn't need live,
|
|
68
|
+
* frame-by-frame updates (a bot integration, a batch job, a callback-
|
|
69
|
+
* style webhook handler) — same underlying turn, same event vocabulary in
|
|
70
|
+
* `events`, just delivered all at once instead of incrementally. */
|
|
71
|
+
export declare function sendMessage(baseUrl: string, agent: string, message: string, options?: RequestOptions): Promise<MessageResult>;
|
|
72
|
+
export declare function approveCall(baseUrl: string, id: string, options?: RequestOptions): Promise<MessageResult>;
|
|
73
|
+
export declare function denyCall(baseUrl: string, id: string, options?: RequestOptions): Promise<MessageResult>;
|
|
74
|
+
export declare function answerQuestion(baseUrl: string, id: string, answer: string, options?: RequestOptions): Promise<MessageResult>;
|
|
75
|
+
/** GET /agents/:name/sessions/:id — rehydrates a conversation's stored
|
|
76
|
+
* history, e.g. after a page reload lost whatever in-memory state a React/
|
|
77
|
+
* Vue app was holding (mirrors web/playground.ts's own resumeSession). */
|
|
78
|
+
export declare function getSessionHistory(baseUrl: string, agent: string, sessionId: string, options?: Pick<RequestOptions, 'headers' | 'signal'>): Promise<{
|
|
79
|
+
sessionId: string;
|
|
80
|
+
history: Message[];
|
|
81
|
+
}>;
|
|
82
|
+
/** The streaming half of the protocol — an async generator yielding one
|
|
83
|
+
* LoopEvent per SSE frame as it arrives, always starting with `session`
|
|
84
|
+
* and ending with `done` or `error` (see loop-events.ts's own header
|
|
85
|
+
* comment). `for await` is the natural fit for a React effect or Vue
|
|
86
|
+
* composable driving off this directly:
|
|
87
|
+
*
|
|
88
|
+
* for await (const event of streamMessage(baseUrl, agent, text, { signal })) {
|
|
89
|
+
* setEvents((prev) => [...prev, event])
|
|
90
|
+
* if (event.type === 'done' || event.type === 'error') break
|
|
91
|
+
* }
|
|
92
|
+
*
|
|
93
|
+
* Pass an AbortController's signal to let the caller cancel mid-stream
|
|
94
|
+
* (a React effect's own cleanup, a Vue component's onUnmounted) — the
|
|
95
|
+
* underlying fetch is aborted and the generator simply stops yielding,
|
|
96
|
+
* same as any other AbortSignal-aware fetch call.
|
|
97
|
+
*/
|
|
98
|
+
export declare function streamMessage(baseUrl: string, agent: string, message: string, options?: RequestOptions): AsyncGenerator<LoopEvent, void, void>;
|
|
99
|
+
export interface StreamMessageCallbacks {
|
|
100
|
+
onEvent?: (event: LoopEvent) => void;
|
|
101
|
+
onDone?: (event: Extract<LoopEvent, {
|
|
102
|
+
type: 'done';
|
|
103
|
+
}>) => void;
|
|
104
|
+
onError?: (event: Extract<LoopEvent, {
|
|
105
|
+
type: 'error';
|
|
106
|
+
}>) => void;
|
|
107
|
+
}
|
|
108
|
+
/** Callback-flavored wrapper over streamMessage() for a caller that would
|
|
109
|
+
* rather pass handlers into a `useEffect`/lifecycle hook than manage a
|
|
110
|
+
* `for await` loop and its own break condition by hand. Resolves once the
|
|
111
|
+
* stream ends (a 'done'/'error' event, or the connection just closing). */
|
|
112
|
+
export declare function streamMessageWithCallbacks(baseUrl: string, agent: string, message: string, options: RequestOptions, callbacks: StreamMessageCallbacks): Promise<void>;
|
|
113
|
+
export {};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
async function postJson(url, body, options = {}) {
|
|
2
|
+
const res = await fetch(url, {
|
|
3
|
+
method: 'POST',
|
|
4
|
+
headers: { 'content-type': 'application/json', ...options.headers },
|
|
5
|
+
body: JSON.stringify(body),
|
|
6
|
+
signal: options.signal,
|
|
7
|
+
});
|
|
8
|
+
const parsed = (await res.json().catch(() => ({ error: `HTTP ${res.status}` })));
|
|
9
|
+
if (!res.ok)
|
|
10
|
+
throw new Error(parsed.error ?? `HTTP ${res.status}`);
|
|
11
|
+
return parsed;
|
|
12
|
+
}
|
|
13
|
+
function messageBody(message, options) {
|
|
14
|
+
return { message, ...(options.sessionId ? { sessionId: options.sessionId } : {}), ...options.body };
|
|
15
|
+
}
|
|
16
|
+
/** The non-streaming half of the protocol — one request, one response,
|
|
17
|
+
* either done (SendMessageResult) or needing a decision (PendingResult).
|
|
18
|
+
* Reach for this over streamMessage() when the caller doesn't need live,
|
|
19
|
+
* frame-by-frame updates (a bot integration, a batch job, a callback-
|
|
20
|
+
* style webhook handler) — same underlying turn, same event vocabulary in
|
|
21
|
+
* `events`, just delivered all at once instead of incrementally. */
|
|
22
|
+
export function sendMessage(baseUrl, agent, message, options = {}) {
|
|
23
|
+
return postJson(`${baseUrl}/agents/${encodeURIComponent(agent)}/messages`, messageBody(message, options), options);
|
|
24
|
+
}
|
|
25
|
+
export function approveCall(baseUrl, id, options = {}) {
|
|
26
|
+
return postJson(`${baseUrl}/approvals/${encodeURIComponent(id)}/approve`, undefined, options);
|
|
27
|
+
}
|
|
28
|
+
export function denyCall(baseUrl, id, options = {}) {
|
|
29
|
+
return postJson(`${baseUrl}/approvals/${encodeURIComponent(id)}/deny`, undefined, options);
|
|
30
|
+
}
|
|
31
|
+
export function answerQuestion(baseUrl, id, answer, options = {}) {
|
|
32
|
+
return postJson(`${baseUrl}/questions/${encodeURIComponent(id)}/answer`, { answer }, options);
|
|
33
|
+
}
|
|
34
|
+
/** GET /agents/:name/sessions/:id — rehydrates a conversation's stored
|
|
35
|
+
* history, e.g. after a page reload lost whatever in-memory state a React/
|
|
36
|
+
* Vue app was holding (mirrors web/playground.ts's own resumeSession). */
|
|
37
|
+
export async function getSessionHistory(baseUrl, agent, sessionId, options = {}) {
|
|
38
|
+
const res = await fetch(`${baseUrl}/agents/${encodeURIComponent(agent)}/sessions/${encodeURIComponent(sessionId)}`, {
|
|
39
|
+
headers: options.headers,
|
|
40
|
+
signal: options.signal,
|
|
41
|
+
});
|
|
42
|
+
const body = (await res.json());
|
|
43
|
+
if (!res.ok)
|
|
44
|
+
throw new Error(body.error ?? `HTTP ${res.status}`);
|
|
45
|
+
return body;
|
|
46
|
+
}
|
|
47
|
+
function parseSseFrame(frame) {
|
|
48
|
+
let dataLine = '';
|
|
49
|
+
for (const line of frame.split('\n')) {
|
|
50
|
+
if (line.startsWith('data: '))
|
|
51
|
+
dataLine = line.slice('data: '.length);
|
|
52
|
+
}
|
|
53
|
+
if (!dataLine)
|
|
54
|
+
return undefined;
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(dataLine);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** The streaming half of the protocol — an async generator yielding one
|
|
63
|
+
* LoopEvent per SSE frame as it arrives, always starting with `session`
|
|
64
|
+
* and ending with `done` or `error` (see loop-events.ts's own header
|
|
65
|
+
* comment). `for await` is the natural fit for a React effect or Vue
|
|
66
|
+
* composable driving off this directly:
|
|
67
|
+
*
|
|
68
|
+
* for await (const event of streamMessage(baseUrl, agent, text, { signal })) {
|
|
69
|
+
* setEvents((prev) => [...prev, event])
|
|
70
|
+
* if (event.type === 'done' || event.type === 'error') break
|
|
71
|
+
* }
|
|
72
|
+
*
|
|
73
|
+
* Pass an AbortController's signal to let the caller cancel mid-stream
|
|
74
|
+
* (a React effect's own cleanup, a Vue component's onUnmounted) — the
|
|
75
|
+
* underlying fetch is aborted and the generator simply stops yielding,
|
|
76
|
+
* same as any other AbortSignal-aware fetch call.
|
|
77
|
+
*/
|
|
78
|
+
export async function* streamMessage(baseUrl, agent, message, options = {}) {
|
|
79
|
+
const res = await fetch(`${baseUrl}/agents/${encodeURIComponent(agent)}/messages/stream`, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: { 'content-type': 'application/json', ...options.headers },
|
|
82
|
+
body: JSON.stringify(messageBody(message, options)),
|
|
83
|
+
signal: options.signal,
|
|
84
|
+
});
|
|
85
|
+
const contentType = res.headers.get('content-type') ?? '';
|
|
86
|
+
if (!contentType.includes('text/event-stream')) {
|
|
87
|
+
const body = (await res.json().catch(() => ({ error: `HTTP ${res.status}` })));
|
|
88
|
+
yield { type: 'error', error: body.error ?? `HTTP ${res.status}` };
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!res.body)
|
|
92
|
+
return;
|
|
93
|
+
const reader = res.body.getReader();
|
|
94
|
+
const decoder = new TextDecoder();
|
|
95
|
+
let buffer = '';
|
|
96
|
+
for (;;) {
|
|
97
|
+
const { done, value } = await reader.read();
|
|
98
|
+
if (done)
|
|
99
|
+
break;
|
|
100
|
+
buffer += decoder.decode(value, { stream: true });
|
|
101
|
+
let idx;
|
|
102
|
+
while ((idx = buffer.indexOf('\n\n')) !== -1) {
|
|
103
|
+
const frame = buffer.slice(0, idx);
|
|
104
|
+
buffer = buffer.slice(idx + 2);
|
|
105
|
+
const event = parseSseFrame(frame);
|
|
106
|
+
if (event)
|
|
107
|
+
yield event;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** Callback-flavored wrapper over streamMessage() for a caller that would
|
|
112
|
+
* rather pass handlers into a `useEffect`/lifecycle hook than manage a
|
|
113
|
+
* `for await` loop and its own break condition by hand. Resolves once the
|
|
114
|
+
* stream ends (a 'done'/'error' event, or the connection just closing). */
|
|
115
|
+
export async function streamMessageWithCallbacks(baseUrl, agent, message, options, callbacks) {
|
|
116
|
+
for await (const event of streamMessage(baseUrl, agent, message, options)) {
|
|
117
|
+
callbacks.onEvent?.(event);
|
|
118
|
+
if (event.type === 'done')
|
|
119
|
+
callbacks.onDone?.(event);
|
|
120
|
+
else if (event.type === 'error')
|
|
121
|
+
callbacks.onError?.(event);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { AgentConfig, AgentModelConfig } from '#core/agent-config.js';
|
|
2
|
+
import type { ModelCall } from '#core/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
|
+
/** Builds a lazy, memoized createModelCall from AgentConfig.model, for a
|
|
9
|
+
* module that doesn't export its own — see that field's own doc comment.
|
|
10
|
+
* The provider's SDK module is imported here, eagerly, but that's cheap
|
|
11
|
+
* and safe with no API key at all: importing model-calls/*.ts only
|
|
12
|
+
* defines functions, it never constructs a client at module top level
|
|
13
|
+
* (see e.g. createAnthropicModelCall's own body) — so this can't fail
|
|
14
|
+
* discovery the way eagerly building the actual client would. The real
|
|
15
|
+
* client is only built the first time the returned function is actually
|
|
16
|
+
* called, and reused after that, the same "don't crash the whole server
|
|
17
|
+
* over one agent's missing API key, don't rebuild per call" reasoning
|
|
18
|
+
* agents/customer-service/index.ts's own hand-written createModelCall
|
|
19
|
+
* used before this existed.
|
|
20
|
+
*
|
|
21
|
+
* Exported so a caller updating an *already-registered* agent's model
|
|
22
|
+
* (see agent-file-admin.ts's editAgentFile, applied live via
|
|
23
|
+
* core/agent-registry.ts's updateAgent) can rebuild just this one closure
|
|
24
|
+
* without re-importing the whole module — Node's ESM loader caches an
|
|
25
|
+
* already-imported module forever, so there's no supported way to
|
|
26
|
+
* re-import a changed agent file's own createModelCall directly; this is
|
|
27
|
+
* the one piece of it that's meaningfully regenerable on its own. */
|
|
28
|
+
export declare function synthesizeCreateModelCall(model: AgentModelConfig): Promise<() => ModelCall>;
|
|
29
|
+
/** Imports one agent module at `path` and resolves it to an AgentModule —
|
|
30
|
+
* the single-file version of what discoverAgents does over a whole
|
|
31
|
+
* directory, factored out so run-agent.ts's subagent loader (see
|
|
32
|
+
* agent-as-tool.ts / loadSubagentAsTools) can load one `subagents/<name>/
|
|
33
|
+
* index.ts` at a time without duplicating this resolution logic.
|
|
34
|
+
* `label` is only used in error messages — discoverAgents passes
|
|
35
|
+
* `'<file> in <dir>'`, callers loading a single known path can just pass
|
|
36
|
+
* that path. */
|
|
37
|
+
export declare function loadAgentModule(path: string, label: string): Promise<AgentModule>;
|
|
38
|
+
/** Scans `dir` for agent modules — direct `.ts`/`.js` files, or
|
|
39
|
+
* subdirectories with an `index.ts`/`index.js` (see resolveModulePath) —
|
|
40
|
+
* each expected to export `config`, and either its own `createModelCall`
|
|
41
|
+
* or a `config.model` for one to be synthesized from (see
|
|
42
|
+
* AgentModelConfig's own doc comment). Throws, rather than skipping, on a
|
|
43
|
+
* module that doesn't match that shape or a duplicate AgentConfig.name —
|
|
44
|
+
* both are almost always a bug, not something to silently ignore. */
|
|
45
|
+
export declare function discoverAgents(dir: string): Promise<Map<string, AgentModule>>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Scans a directory for agent modules and builds a name -> module map from
|
|
2
2
|
// it — the thing that makes "drop a new file in agents/" enough to
|
|
3
3
|
// register an agent, no hand-written import-and-add-one-line edit needed
|
|
4
|
-
// anywhere else. See agent-registry.ts for the one place this repo's own
|
|
4
|
+
// anywhere else. See core/agent-registry.ts for the one place this repo's own
|
|
5
5
|
// reference app actually calls it.
|
|
6
6
|
//
|
|
7
7
|
// Keyed by AgentConfig.name, not the filename — that's the same identity
|
|
@@ -28,8 +28,16 @@ function isRawAgentModule(mod) {
|
|
|
28
28
|
* called, and reused after that, the same "don't crash the whole server
|
|
29
29
|
* over one agent's missing API key, don't rebuild per call" reasoning
|
|
30
30
|
* agents/customer-service/index.ts's own hand-written createModelCall
|
|
31
|
-
* used before this existed.
|
|
32
|
-
|
|
31
|
+
* used before this existed.
|
|
32
|
+
*
|
|
33
|
+
* Exported so a caller updating an *already-registered* agent's model
|
|
34
|
+
* (see agent-file-admin.ts's editAgentFile, applied live via
|
|
35
|
+
* core/agent-registry.ts's updateAgent) can rebuild just this one closure
|
|
36
|
+
* without re-importing the whole module — Node's ESM loader caches an
|
|
37
|
+
* already-imported module forever, so there's no supported way to
|
|
38
|
+
* re-import a changed agent file's own createModelCall directly; this is
|
|
39
|
+
* the one piece of it that's meaningfully regenerable on its own. */
|
|
40
|
+
export async function synthesizeCreateModelCall(model) {
|
|
33
41
|
let cached;
|
|
34
42
|
if (model.provider === 'anthropic') {
|
|
35
43
|
const { createAnthropicModelCall } = await import('./model-calls/anthropic-model-call.js');
|
|
@@ -66,7 +74,7 @@ function resolveModulePath(dir, entry) {
|
|
|
66
74
|
/** Imports one agent module at `path` and resolves it to an AgentModule —
|
|
67
75
|
* the single-file version of what discoverAgents does over a whole
|
|
68
76
|
* directory, factored out so run-agent.ts's subagent loader (see
|
|
69
|
-
* agent-as-tool.ts /
|
|
77
|
+
* agent-as-tool.ts / loadSubagentAsTools) can load one `subagents/<name>/
|
|
70
78
|
* index.ts` at a time without duplicating this resolution logic.
|
|
71
79
|
* `label` is only used in error messages — discoverAgents passes
|
|
72
80
|
* `'<file> in <dir>'`, callers loading a single known path can just pass
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import type { Decision } from 'actauth';
|
|
2
|
+
import type { ToolDefinition } from './agent-config.js';
|
|
3
|
+
export interface ResolvedGatewayTool {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
input_schema: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
export interface ComposioGatewayToolEntry {
|
|
9
|
+
provider: 'composio';
|
|
10
|
+
/** Namespaces every tool this source produces (`${name}_${slug}`,
|
|
11
|
+
* mcpplug's own convention) — must be unique among this agent's own
|
|
12
|
+
* sources. */
|
|
13
|
+
name: string;
|
|
14
|
+
slugs: string[];
|
|
15
|
+
/** Override for a non-default binary name, or a stand-in script in
|
|
16
|
+
* tests — see ComposioSourceOptions.cliCommand. Defaults to 'composio'. */
|
|
17
|
+
cliCommand?: string;
|
|
18
|
+
/** {name, description, input_schema} per slug, once resolved — see
|
|
19
|
+
* connectEntry/describeGatewayTools. Populated lazily (a slug just
|
|
20
|
+
* added via addGatewayTool has no entry here yet) and persisted back
|
|
21
|
+
* into this same file (see persistResolvedTools), not held in memory —
|
|
22
|
+
* so a schema, once resolved, survives a server restart instead of
|
|
23
|
+
* costing another ~3s Composio round trip on the next load. Keyed by
|
|
24
|
+
* slug, not by the derived tool name (`${name}_${slug}`), since the
|
|
25
|
+
* slug is what's stable if this source were ever renamed. Hand-editing
|
|
26
|
+
* this is never expected — treat it as generated, the same as
|
|
27
|
+
* describeGatewayTools' own in-memory result used to be before this
|
|
28
|
+
* existed. */
|
|
29
|
+
resolved?: Record<string, ResolvedGatewayTool>;
|
|
30
|
+
}
|
|
31
|
+
export type GatewayToolEntry = ComposioGatewayToolEntry;
|
|
32
|
+
/** A top-level agent's real folder — exported so callers resolving other
|
|
33
|
+
* per-agent paths (adapters/http.ts's describeAgent, computing the same
|
|
34
|
+
* `dir` to hand to loadGatewayToolsFromDir) use the exact same base this
|
|
35
|
+
* module's own registry read/writes do, rather than re-deriving it (e.g.
|
|
36
|
+
* from process.cwd()) and risking the two drifting apart. */
|
|
37
|
+
export declare function agentDir(agentName: string): string;
|
|
38
|
+
/** `dir` is the agent's real folder — `agents/<name>` for a top-level
|
|
39
|
+
* agent, or the nested `agents/<parent>/subagents/<child>` a subagent
|
|
40
|
+
* actually lives in (see run-agent.ts's resolveSubagentConfig, which
|
|
41
|
+
* hit this exact same name-vs-folder distinction for tools/rules/
|
|
42
|
+
* skillsDirs). Missing file is just `[]`, same convention every other
|
|
43
|
+
* folder-form default in this repo gets. */
|
|
44
|
+
export declare function readGatewayToolsFromDir(dir: string): GatewayToolEntry[];
|
|
45
|
+
/** Convenience wrapper over readGatewayToolsFromDir for the common,
|
|
46
|
+
* top-level-agent case — what adapters/http.ts's admin routes actually
|
|
47
|
+
* call, since GET /agents/:name/gateway-tools only ever addresses a
|
|
48
|
+
* top-level agent (getEntry(name) has no notion of a subagent path). */
|
|
49
|
+
export declare function readGatewayTools(agentName: string): GatewayToolEntry[];
|
|
50
|
+
export declare class GatewayToolExistsError extends Error {
|
|
51
|
+
}
|
|
52
|
+
export declare class GatewayToolNotFoundError extends Error {
|
|
53
|
+
}
|
|
54
|
+
/** Whether a gateway tool's name reads as read-only (safe to
|
|
55
|
+
* auto-allow) rather than mutating (should stay at actauth's own
|
|
56
|
+
* default, typically 'ask' or 'deny', until a human explicitly grants
|
|
57
|
+
* it) — see addGatewayTool's 'auto' decision mode, which is what
|
|
58
|
+
* actually calls this. The first token that matches either list wins,
|
|
59
|
+
* scanned in order; a name with no recognized verb at all (an
|
|
60
|
+
* inconsistently-named action, or a toolkit this list hasn't seen yet)
|
|
61
|
+
* defaults to *not* read-only — an unclassifiable tool staying at
|
|
62
|
+
* actauth's default is the safe failure mode, the reverse would silently
|
|
63
|
+
* auto-allow something this heuristic simply doesn't understand. Exported
|
|
64
|
+
* for testing and because adapters/http.ts's picker UI may want to show
|
|
65
|
+
* the same classification before a tool is even added. */
|
|
66
|
+
export declare function isReadOnlyToolName(toolName: string): boolean;
|
|
67
|
+
/** 'auto' resolves per-tool via isReadOnlyToolName instead of one fixed
|
|
68
|
+
* decision for a whole batch — see addGatewayTool's own doc comment for
|
|
69
|
+
* why a single decision can't safely cover "select all" against a
|
|
70
|
+
* toolkit with dozens of actions of very different risk. */
|
|
71
|
+
export type GatewayToolDecision = Decision | 'auto';
|
|
72
|
+
/** Registers a new gateway tool for `agentName` (top-level only — see
|
|
73
|
+
* readGatewayTools), or, if a source with this exact `name` is already
|
|
74
|
+
* registered, merges `entry.slugs` into it (deduped) instead of
|
|
75
|
+
* rejecting the call — the name is just a namespace label, not a
|
|
76
|
+
* one-shot slot; reusing it to add more tools under the same source is
|
|
77
|
+
* the common case (picking a few more actions for an app you already
|
|
78
|
+
* added), not a conflict. It's only a real conflict when the existing
|
|
79
|
+
* entry is for a *different* provider — two different connection
|
|
80
|
+
* configs can't share one name. `decision`, if given, pre-seeds an
|
|
81
|
+
* actauth rule per tool in *this call's own* `entry.slugs` (see
|
|
82
|
+
* appendActauthRules) — not the merged list, so re-adding an
|
|
83
|
+
* already-registered slug doesn't also re-seed a duplicate rule for it.
|
|
84
|
+
* Omit `decision` to leave every new tool at actauth's own
|
|
85
|
+
* defaultDecision (typically 'deny'), the same "new tools are opt-in,
|
|
86
|
+
* not silently allowed" convention every other tool in this repo
|
|
87
|
+
* follows. `decision: 'auto'` — see appendActauthRules — resolves per
|
|
88
|
+
* tool instead of applying one decision to the whole batch. */
|
|
89
|
+
export declare function addGatewayTool(agentName: string, entry: GatewayToolEntry, decision?: GatewayToolDecision): void;
|
|
90
|
+
/** Removes a gateway tool's registration, along with any actauth rule
|
|
91
|
+
* appendActauthRules previously seeded for it (see
|
|
92
|
+
* removeAutoAddedActauthRules — a hand-authored rule referencing the
|
|
93
|
+
* same tool under a different name is never touched). Removing a tool
|
|
94
|
+
* is expected to remove the permission it was granted too, not leave a
|
|
95
|
+
* rule that governs nothing behind. */
|
|
96
|
+
export declare function removeGatewayTool(agentName: string, sourceName: string): void;
|
|
97
|
+
/** Removes one slug from an existing source — the per-tool remove icon
|
|
98
|
+
* in the Tools tab's Gateway Tools list, as opposed to removeGatewayTool
|
|
99
|
+
* above, which drops the whole source at once. Deletes the source
|
|
100
|
+
* entirely if that was its last slug: a source with zero slugs produces
|
|
101
|
+
* zero tools anyway (mcpplug's connectComposioSource loops per slug, see
|
|
102
|
+
* connectEntry), so there's nothing left worth keeping registered. Same
|
|
103
|
+
* "also remove any rule this tool was auto-granted" behavior as
|
|
104
|
+
* removeGatewayTool — see its own doc comment and
|
|
105
|
+
* removeAutoAddedActauthRules. */
|
|
106
|
+
export declare function removeGatewayToolSlug(agentName: string, sourceName: string, slug: string): void;
|
|
107
|
+
export interface GatewayToolStatus {
|
|
108
|
+
entry: GatewayToolEntry;
|
|
109
|
+
status: 'ok' | 'error';
|
|
110
|
+
tools: Array<{
|
|
111
|
+
name: string;
|
|
112
|
+
description: string;
|
|
113
|
+
}>;
|
|
114
|
+
error?: string;
|
|
115
|
+
}
|
|
116
|
+
/** The admin page's read model — every registered source for `agentName`,
|
|
117
|
+
* each actually connected to and reported on individually, same
|
|
118
|
+
* fail-open-per-source behavior loadGatewayToolsFromDir has (see its own
|
|
119
|
+
* doc comment). The difference is what happens with a failure: this
|
|
120
|
+
* function surfaces it explicitly, per source, since an operator looking
|
|
121
|
+
* at this page needs to know *which* source is broken and why — where
|
|
122
|
+
* loadGatewayToolsFromDir just logs and drops it, since runAgent() has no
|
|
123
|
+
* structured place to hand a per-source error to.
|
|
124
|
+
*
|
|
125
|
+
* No in-memory cache here (or anywhere in this file, for this) — the
|
|
126
|
+
* cache *is* gateway-tools.yml's own `resolved` field (see
|
|
127
|
+
* ComposioGatewayToolEntry's own doc comment and connectEntry, which this
|
|
128
|
+
* shares that field with), so it survives a restart instead of being
|
|
129
|
+
* lost on one. Only genuinely-unresolved slugs pay for a live fetch, and
|
|
130
|
+
* for those this still enriches with Composio's real catalog description
|
|
131
|
+
* (see withRealComposioDescriptions) before persisting — unlike
|
|
132
|
+
* connectEntry, which doesn't (see that function's own reasoning), this
|
|
133
|
+
* is an occasional operator action, not runAgent()'s hot path, so paying
|
|
134
|
+
* for the better description once, at resolution time, is worth it here.
|
|
135
|
+
* Pass `force: true` (the admin page's own "Refresh" button) to treat
|
|
136
|
+
* every slug as unresolved and overwrite their persisted entries with a
|
|
137
|
+
* fresh live check — e.g. after reconnecting an account, or on the rare
|
|
138
|
+
* chance a schema changed upstream without gateway-tools.yml itself
|
|
139
|
+
* changing. */
|
|
140
|
+
export declare function describeGatewayTools(agentName: string, options?: {
|
|
141
|
+
force?: boolean;
|
|
142
|
+
}): Promise<GatewayToolStatus[]>;
|
|
143
|
+
/** Resolves agents/<name>/gateway-tools.yml (or the equivalent nested path
|
|
144
|
+
* for a subagent — see readGatewayToolsFromDir) into real ToolDefinitions,
|
|
145
|
+
* for run-agent.ts to merge into the rest of an agent's tools. Missing
|
|
146
|
+
* file is `[]`, no cache entry created.
|
|
147
|
+
*
|
|
148
|
+
* Fails open per source: one broken/unreachable gateway (Composio down,
|
|
149
|
+
* a CLI not authenticated) is logged and contributes no tools, rather
|
|
150
|
+
* than rejecting the whole call — the same reasoning a single tool's own
|
|
151
|
+
* `execute()` failing doesn't crash runAgent's whole turn (ToolLane
|
|
152
|
+
* reports it as an is_error tool_result instead). Without this, an agent
|
|
153
|
+
* with an unrelated broken integration would fail *every* request, tools
|
|
154
|
+
* it actually needs included — confirmed live: a run-agent.test.ts test
|
|
155
|
+
* that reuses the real 'file-agent' name purely to exercise the
|
|
156
|
+
* skillsDirs default started silently depending on the real `composio`
|
|
157
|
+
* CLI/network the moment agents/file-agent/gateway-tools.yml existed,
|
|
158
|
+
* before this fix made a failed (or slow/unavailable) source harmless
|
|
159
|
+
* instead of fatal. */
|
|
160
|
+
export declare function loadGatewayToolsFromDir(dir: string): Promise<ToolDefinition[]>;
|
|
161
|
+
export interface ComposioConnection {
|
|
162
|
+
toolkit: string;
|
|
163
|
+
status: string;
|
|
164
|
+
alias: string | null;
|
|
165
|
+
wordId: string | null;
|
|
166
|
+
}
|
|
167
|
+
/** Flattens `composio connections list`'s own shape (a map of toolkit
|
|
168
|
+
* slug -> array of accounts) into one array — easier for a caller (the
|
|
169
|
+
* add-source picker) to filter/sort without knowing that shape itself.
|
|
170
|
+
* Every toolkit with at least one connected account appears, `status`
|
|
171
|
+
* and all; deciding what counts as "usable" (typically just `'ACTIVE'`)
|
|
172
|
+
* is left to the caller; a CLI/auth failure propagates as a normal
|
|
173
|
+
* rejection — see runComposioCli. */
|
|
174
|
+
export declare function listComposioConnections(cliCommand?: string): Promise<ComposioConnection[]>;
|
|
175
|
+
export interface ComposioToolInfo {
|
|
176
|
+
slug: string;
|
|
177
|
+
name: string;
|
|
178
|
+
description: string;
|
|
179
|
+
}
|
|
180
|
+
/** What `composio tools list <toolkit>` offers for one already-connected
|
|
181
|
+
* app — the picker's second step, once a toolkit is chosen from
|
|
182
|
+
* listComposioConnections. Every action a toolkit exposes, not just ones
|
|
183
|
+
* already registered as a gateway tool source — the picker's whole point
|
|
184
|
+
* is showing what's *available* to add. `--limit 1000` (the CLI's own
|
|
185
|
+
* max) is explicit rather than left to the CLI's own default: without
|
|
186
|
+
* it, `composio tools list github` silently caps at 30 results — out of
|
|
187
|
+
* GitHub's real ~900, and alphabetically sorted, so a picker session
|
|
188
|
+
* would only ever see a narrow, unrepresentative slice (every result
|
|
189
|
+
* starting with "A" — ABORT/ACCEPT/ADD/API/APPROVE/ASSIGN — almost all
|
|
190
|
+
* mutating verbs), not "every tool this toolkit has" the doc comment
|
|
191
|
+
* above already promises. Confirmed live against the real CLI: no
|
|
192
|
+
* --limit gave 30, --limit 1000 gave 893. */
|
|
193
|
+
export declare function listComposioTools(toolkit: string, cliCommand?: string): Promise<ComposioToolInfo[]>;
|
|
194
|
+
export interface ComposioAuthStatus {
|
|
195
|
+
connected: boolean;
|
|
196
|
+
email?: string;
|
|
197
|
+
org?: string;
|
|
198
|
+
}
|
|
199
|
+
/** Whether the composio CLI itself (not any one connected app — see
|
|
200
|
+
* listComposioConnections for that) is authenticated on this machine.
|
|
201
|
+
* `composio whoami` succeeds once `composio login` has run and fails
|
|
202
|
+
* otherwise, so a failure here is read as "not connected", not
|
|
203
|
+
* propagated as an error the way listComposioConnections' own CLI
|
|
204
|
+
* failures are — this is a normal, expected state for an environment
|
|
205
|
+
* that just hasn't been logged in yet, not a bug. Deliberately
|
|
206
|
+
* read-only: login is an interactive, machine-wide CLI session (opens a
|
|
207
|
+
* browser, affects every agent's gateway tools, not just one web
|
|
208
|
+
* request), so the global config page's Gateways panel that calls this
|
|
209
|
+
* only ever shows status + instructions, never drives login/logout
|
|
210
|
+
* itself. */
|
|
211
|
+
export declare function getComposioAuthStatus(cliCommand?: string): Promise<ComposioAuthStatus>;
|
|
212
|
+
/** `composio logout` — already non-interactive on its own (see its own
|
|
213
|
+
* --help), unlike login. Machine-wide: this deauthenticates the CLI for
|
|
214
|
+
* every agent's gateway tools on this machine, not just the browser tab
|
|
215
|
+
* that clicked Disconnect — same as running it in a terminal yourself.
|
|
216
|
+
*
|
|
217
|
+
* There's no connectComposioAccount counterpart: `composio login`'s only
|
|
218
|
+
* non-interactive path is `--user-api-key`, which turned out not to be
|
|
219
|
+
* what an operator actually has on hand — the key Composio's own
|
|
220
|
+
* dashboard hands out for MCP client auth (sent via an x-consumer-api-key
|
|
221
|
+
* header) isn't the same credential, and 401s against this flag.
|
|
222
|
+
* Composio's own documented path is plain, interactive `composio login`
|
|
223
|
+
* (opens a browser) after installing the CLI — not something a web
|
|
224
|
+
* request can drive, so the Gateways panel shows that as instructions
|
|
225
|
+
* instead of a form; see its own comment in web/global-config-page.ts. */
|
|
226
|
+
export declare function disconnectComposioAccount(cliCommand?: string): Promise<void>;
|