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.
Files changed (40) hide show
  1. package/README.md +67 -6
  2. package/core/system-skills/composio-large-outputs/SKILL.md +25 -0
  3. package/dist/{cli.d.ts → bin/cli.d.ts} +27 -6
  4. package/dist/bin/cli.js +384 -0
  5. package/dist/{agent-as-tool.d.ts → core/agent-as-tool.d.ts} +2 -2
  6. package/dist/{agent-as-tool.js → core/agent-as-tool.js} +1 -1
  7. package/dist/core/client.d.ts +113 -0
  8. package/dist/core/client.js +123 -0
  9. package/dist/core/discover-agents.d.ts +45 -0
  10. package/dist/{discover-agents.js → core/discover-agents.js} +12 -4
  11. package/dist/core/gateway-tools.d.ts +226 -0
  12. package/dist/core/gateway-tools.js +660 -0
  13. package/dist/core/loop-events.d.ts +168 -0
  14. package/dist/core/loop-events.js +1 -0
  15. package/dist/{model-calls → core/model-calls}/anthropic-model-call.d.ts +1 -1
  16. package/dist/{model-calls → core/model-calls}/deepseek-model-call.d.ts +1 -1
  17. package/dist/{model-calls → core/model-calls}/openai-model-call.d.ts +1 -1
  18. package/dist/core/run-agent.d.ts +182 -0
  19. package/dist/{run-agent.js → core/run-agent.js} +233 -51
  20. package/dist/{session-store.d.ts → core/session-store.d.ts} +13 -1
  21. package/dist/{session-store.js → core/session-store.js} +19 -0
  22. package/dist/core/system-tools/ask_user.d.ts +72 -0
  23. package/dist/core/system-tools/ask_user.js +136 -0
  24. package/dist/core/system-tools/index.d.ts +3 -0
  25. package/dist/core/system-tools/index.js +12 -0
  26. package/dist/core/system-tools/read_file.d.ts +28 -0
  27. package/dist/core/system-tools/read_file.js +64 -0
  28. package/dist/index.d.ts +19 -16
  29. package/dist/index.js +16 -9
  30. package/package.json +33 -25
  31. package/dist/cli.js +0 -191
  32. package/dist/discover-agents.d.ts +0 -24
  33. package/dist/run-agent.d.ts +0 -92
  34. /package/dist/{agent-config.d.ts → core/agent-config.d.ts} +0 -0
  35. /package/dist/{agent-config.js → core/agent-config.js} +0 -0
  36. /package/dist/{model-calls → core/model-calls}/anthropic-model-call.js +0 -0
  37. /package/dist/{model-calls → core/model-calls}/deepseek-model-call.js +0 -0
  38. /package/dist/{model-calls → core/model-calls}/openai-model-call.js +0 -0
  39. /package/dist/{vector-index.d.ts → core/vector-index.d.ts} +0 -0
  40. /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
- async function synthesizeCreateModelCall(model) {
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 / loadSubagentTools) can load one `subagents/<name>/
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>;