loopengine 0.0.18 → 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 +34 -3
- package/core/system-skills/composio-large-outputs/SKILL.md +25 -0
- package/dist/{cli.d.ts → bin/cli.d.ts} +25 -4
- package/dist/{cli.js → bin/cli.js} +131 -16
- 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} +11 -3
- package/dist/{gateway-tools.d.ts → core/gateway-tools.d.ts} +38 -6
- package/dist/{gateway-tools.js → core/gateway-tools.js} +140 -28
- 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/{run-agent.d.ts → core/run-agent.d.ts} +77 -9
- package/dist/{run-agent.js → core/run-agent.js} +213 -44
- 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 +32 -26
- package/dist/discover-agents.d.ts +0 -24
- /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,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');
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { Decision } from 'actauth';
|
|
2
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
|
+
}
|
|
3
8
|
export interface ComposioGatewayToolEntry {
|
|
4
9
|
provider: 'composio';
|
|
5
10
|
/** Namespaces every tool this source produces (`${name}_${slug}`,
|
|
@@ -10,6 +15,18 @@ export interface ComposioGatewayToolEntry {
|
|
|
10
15
|
/** Override for a non-default binary name, or a stand-in script in
|
|
11
16
|
* tests — see ComposioSourceOptions.cliCommand. Defaults to 'composio'. */
|
|
12
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>;
|
|
13
30
|
}
|
|
14
31
|
export type GatewayToolEntry = ComposioGatewayToolEntry;
|
|
15
32
|
/** A top-level agent's real folder — exported so callers resolving other
|
|
@@ -103,11 +120,26 @@ export interface GatewayToolStatus {
|
|
|
103
120
|
* function surfaces it explicitly, per source, since an operator looking
|
|
104
121
|
* at this page needs to know *which* source is broken and why — where
|
|
105
122
|
* loadGatewayToolsFromDir just logs and drops it, since runAgent() has no
|
|
106
|
-
* structured place to hand a per-source error to.
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
|
|
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[]>;
|
|
111
143
|
/** Resolves agents/<name>/gateway-tools.yml (or the equivalent nested path
|
|
112
144
|
* for a subagent — see readGatewayToolsFromDir) into real ToolDefinitions,
|
|
113
145
|
* for run-agent.ts to merge into the rest of an agent's tools. Missing
|
|
@@ -190,5 +222,5 @@ export declare function getComposioAuthStatus(cliCommand?: string): Promise<Comp
|
|
|
190
222
|
* Composio's own documented path is plain, interactive `composio login`
|
|
191
223
|
* (opens a browser) after installing the CLI — not something a web
|
|
192
224
|
* request can drive, so the Gateways panel shows that as instructions
|
|
193
|
-
* instead of a form; see its own comment in
|
|
225
|
+
* instead of a form; see its own comment in web/global-config-page.ts. */
|
|
194
226
|
export declare function disconnectComposioAccount(cliCommand?: string): Promise<void>;
|
|
@@ -15,12 +15,13 @@ import { promisify } from 'node:util';
|
|
|
15
15
|
import { parse as parseYaml, parseDocument, stringify as stringifyYaml } from 'yaml';
|
|
16
16
|
import { connectComposioSource } from 'mcpplug';
|
|
17
17
|
const execFileAsync = promisify(execFile);
|
|
18
|
-
// Same "
|
|
19
|
-
// own agentsRootDir uses
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
18
|
+
// Same "one level up from this file, not process.cwd()" reasoning
|
|
19
|
+
// run-agent.ts's own agentsRootDir uses (this file lives in core/
|
|
20
|
+
// alongside it, with agents/ one level up at repo root either way) — real
|
|
21
|
+
// code (an actauth.yml write, a gateway-tools.yml write) needs to land in
|
|
22
|
+
// the same place a built dist/ would look for it, not wherever the
|
|
23
|
+
// process happened to be launched from.
|
|
24
|
+
const agentsRootDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'agents');
|
|
24
25
|
function gatewayToolsPath(dir) {
|
|
25
26
|
return join(dir, 'gateway-tools.yml');
|
|
26
27
|
}
|
|
@@ -56,6 +57,28 @@ function writeGatewayToolsFile(dir, sources) {
|
|
|
56
57
|
mkdirSync(dir, { recursive: true });
|
|
57
58
|
writeFileSync(gatewayToolsPath(dir), stringifyYaml({ sources }));
|
|
58
59
|
}
|
|
60
|
+
// Re-reads and writes gateway-tools.yml fresh, not off whatever snapshot
|
|
61
|
+
// the caller (connectEntry/describeGatewayTools) originally read — a
|
|
62
|
+
// live resolution can take real seconds (mcpplug's per-slug CLI round
|
|
63
|
+
// trips), long enough for an admin-UI edit (add/remove a slug) to land
|
|
64
|
+
// in between. Merged into the source's existing `resolved`, not
|
|
65
|
+
// replaced, so two overlapping resolutions of different slug subsets
|
|
66
|
+
// (e.g. connectEntry resolving one newly-added slug while
|
|
67
|
+
// describeGatewayTools resolves another) don't clobber each other.
|
|
68
|
+
// Silently a no-op if the source itself is gone by the time this runs —
|
|
69
|
+
// nothing left to persist onto.
|
|
70
|
+
function persistResolvedTools(dir, sourceName, newlyResolved) {
|
|
71
|
+
if (!Object.keys(newlyResolved).length)
|
|
72
|
+
return;
|
|
73
|
+
const sources = readGatewayToolsFromDir(dir);
|
|
74
|
+
const index = sources.findIndex((s) => s.name === sourceName);
|
|
75
|
+
if (index === -1)
|
|
76
|
+
return;
|
|
77
|
+
const existing = sources[index];
|
|
78
|
+
const updated = [...sources];
|
|
79
|
+
updated[index] = { ...existing, resolved: { ...existing.resolved, ...newlyResolved } };
|
|
80
|
+
writeGatewayToolsFile(dir, updated);
|
|
81
|
+
}
|
|
59
82
|
export class GatewayToolExistsError extends Error {
|
|
60
83
|
}
|
|
61
84
|
export class GatewayToolNotFoundError extends Error {
|
|
@@ -304,21 +327,86 @@ export function removeGatewayToolSlug(agentName, sourceName, slug) {
|
|
|
304
327
|
throw new GatewayToolNotFoundError(`No gateway tool '${slug}' registered under '${sourceName}' for '${agentName}'.`);
|
|
305
328
|
}
|
|
306
329
|
const remainingSlugs = sources[index].slugs.filter((s) => s !== slug);
|
|
330
|
+
// Drops the removed slug's own resolved entry too — leaving it behind
|
|
331
|
+
// would just be dead weight (nothing in `slugs` references it anymore,
|
|
332
|
+
// so it can never be read back), and would silently reappear if the
|
|
333
|
+
// exact same slug were ever re-added later, skipping what should be a
|
|
334
|
+
// fresh resolution. Omitted entirely (not `resolved: {}`) once empty —
|
|
335
|
+
// a source that never had any resolved slugs shouldn't gain a stray
|
|
336
|
+
// empty field just because one was removed from it.
|
|
337
|
+
const { [slug]: _removed, ...remainingResolved } = sources[index].resolved ?? {};
|
|
307
338
|
const updated = remainingSlugs.length === 0
|
|
308
339
|
? sources.filter((_, i) => i !== index)
|
|
309
|
-
: sources.map((s, i) =>
|
|
340
|
+
: sources.map((s, i) => {
|
|
341
|
+
if (i !== index)
|
|
342
|
+
return s;
|
|
343
|
+
const next = { ...s, slugs: remainingSlugs };
|
|
344
|
+
if (Object.keys(remainingResolved).length)
|
|
345
|
+
next.resolved = remainingResolved;
|
|
346
|
+
else
|
|
347
|
+
delete next.resolved;
|
|
348
|
+
return next;
|
|
349
|
+
});
|
|
310
350
|
writeGatewayToolsFile(dir, updated);
|
|
311
351
|
removeAutoAddedActauthRules(dir, [`${sources[index].name}_${slug}`]);
|
|
312
352
|
}
|
|
313
|
-
|
|
353
|
+
// Builds a ToolDefinition straight from gateway-tools.yml's own persisted
|
|
354
|
+
// `resolved` entry — no CLI call at all for the schema/description, since
|
|
355
|
+
// those are exactly what's already cached. execute() itself still always
|
|
356
|
+
// shells out fresh (mirrors mcpplug's composio-source.ts own execute
|
|
357
|
+
// closure exactly — resolving *what a tool is* is cacheable, actually
|
|
358
|
+
// *running* it never is).
|
|
359
|
+
function toolFromResolved(slug, resolved, cliCommand) {
|
|
360
|
+
return {
|
|
361
|
+
name: resolved.name,
|
|
362
|
+
description: resolved.description,
|
|
363
|
+
input_schema: resolved.input_schema,
|
|
364
|
+
execute: async (input) => {
|
|
365
|
+
const result = (await runComposioCli(cliCommand ?? 'composio', ['execute', slug, '-d', JSON.stringify(input)]));
|
|
366
|
+
if (result.successful === false || result.error) {
|
|
367
|
+
throw new Error(`composio tool ${slug} failed: ${JSON.stringify(result.error ?? result)}`);
|
|
368
|
+
}
|
|
369
|
+
return result.data ?? result;
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
// The only thing that still needs mcpplug/a live Composio round trip —
|
|
374
|
+
// slugs with no persisted resolution yet (just registered, or a `force`
|
|
375
|
+
// re-check). Returns [] without touching the CLI at all when there's
|
|
376
|
+
// nothing left to resolve, which is the common case once a source's
|
|
377
|
+
// slugs have all been resolved once.
|
|
378
|
+
async function fetchUnresolvedSlugs(entry, slugs) {
|
|
379
|
+
if (!slugs.length)
|
|
380
|
+
return [];
|
|
381
|
+
const source = await connectComposioSource(entry.name, { slugs, cliCommand: entry.cliCommand });
|
|
382
|
+
try {
|
|
383
|
+
return await source.loadTools();
|
|
384
|
+
}
|
|
385
|
+
finally {
|
|
386
|
+
await source.close();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/** Resolves `entry` into real ToolDefinitions for run-agent.ts to merge
|
|
390
|
+
* in, preferring gateway-tools.yml's own persisted `resolved` data (see
|
|
391
|
+
* ComposioGatewayToolEntry's own doc comment) over asking Composio again —
|
|
392
|
+
* mcpplug's connectComposioSource (and the ~3s-per-slug CLI round trip it
|
|
393
|
+
* costs) is only ever invoked for slugs that aren't resolved yet. Newly
|
|
394
|
+
* resolved slugs are persisted back before returning, so the *next* call
|
|
395
|
+
* (even after a restart) skips the live fetch entirely too. */
|
|
396
|
+
async function connectEntry(entry, dir) {
|
|
314
397
|
if (entry.provider === 'composio') {
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
398
|
+
const resolved = entry.resolved ?? {};
|
|
399
|
+
const cachedTools = entry.slugs.filter((slug) => resolved[slug]).map((slug) => toolFromResolved(slug, resolved[slug], entry.cliCommand));
|
|
400
|
+
const unresolvedSlugs = entry.slugs.filter((slug) => !resolved[slug]);
|
|
401
|
+
const freshTools = await fetchUnresolvedSlugs(entry, unresolvedSlugs);
|
|
402
|
+
if (freshTools.length) {
|
|
403
|
+
const newlyResolved = {};
|
|
404
|
+
freshTools.forEach((tool, i) => {
|
|
405
|
+
newlyResolved[unresolvedSlugs[i]] = { name: tool.name, description: tool.description, input_schema: tool.input_schema };
|
|
406
|
+
});
|
|
407
|
+
persistResolvedTools(dir, entry.name, newlyResolved);
|
|
321
408
|
}
|
|
409
|
+
return [...cachedTools, ...freshTools];
|
|
322
410
|
}
|
|
323
411
|
// Unreachable while GatewayToolEntry is a union of one — the check (and
|
|
324
412
|
// the cast it needs) starts earning its keep the moment a second
|
|
@@ -334,9 +422,10 @@ async function connectEntry(entry) {
|
|
|
334
422
|
* <toolkit>`) has a genuinely useful one instead (e.g. "Tool to abort a
|
|
335
423
|
* repository migration that is queued or in progress. Use when you need
|
|
336
424
|
* to cancel an ongoing migration operation.") — worth fetching for
|
|
337
|
-
* describeGatewayTools specifically
|
|
338
|
-
*
|
|
339
|
-
* Deliberately *not* applied inside
|
|
425
|
+
* describeGatewayTools specifically — only paid once per slug, right
|
|
426
|
+
* before persisting it (see ComposioGatewayToolEntry's own `resolved`
|
|
427
|
+
* doc comment), not on every load. Deliberately *not* applied inside
|
|
428
|
+
* connectEntry itself, even though
|
|
340
429
|
* that would also improve what the model actually sees at runtime —
|
|
341
430
|
* connectEntry is loadGatewayToolsFromDir's hot path, and that function's
|
|
342
431
|
* own doc comment already explicitly rejects paying any extra
|
|
@@ -378,18 +467,41 @@ async function withRealComposioDescriptions(tools, slugs, cliCommand) {
|
|
|
378
467
|
* function surfaces it explicitly, per source, since an operator looking
|
|
379
468
|
* at this page needs to know *which* source is broken and why — where
|
|
380
469
|
* loadGatewayToolsFromDir just logs and drops it, since runAgent() has no
|
|
381
|
-
* structured place to hand a per-source error to.
|
|
382
|
-
*
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
|
|
470
|
+
* structured place to hand a per-source error to.
|
|
471
|
+
*
|
|
472
|
+
* No in-memory cache here (or anywhere in this file, for this) — the
|
|
473
|
+
* cache *is* gateway-tools.yml's own `resolved` field (see
|
|
474
|
+
* ComposioGatewayToolEntry's own doc comment and connectEntry, which this
|
|
475
|
+
* shares that field with), so it survives a restart instead of being
|
|
476
|
+
* lost on one. Only genuinely-unresolved slugs pay for a live fetch, and
|
|
477
|
+
* for those this still enriches with Composio's real catalog description
|
|
478
|
+
* (see withRealComposioDescriptions) before persisting — unlike
|
|
479
|
+
* connectEntry, which doesn't (see that function's own reasoning), this
|
|
480
|
+
* is an occasional operator action, not runAgent()'s hot path, so paying
|
|
481
|
+
* for the better description once, at resolution time, is worth it here.
|
|
482
|
+
* Pass `force: true` (the admin page's own "Refresh" button) to treat
|
|
483
|
+
* every slug as unresolved and overwrite their persisted entries with a
|
|
484
|
+
* fresh live check — e.g. after reconnecting an account, or on the rare
|
|
485
|
+
* chance a schema changed upstream without gateway-tools.yml itself
|
|
486
|
+
* changing. */
|
|
487
|
+
export async function describeGatewayTools(agentName, options = {}) {
|
|
488
|
+
const dir = agentDir(agentName);
|
|
386
489
|
const entries = readGatewayTools(agentName);
|
|
387
490
|
return Promise.all(entries.map(async (entry) => {
|
|
388
491
|
try {
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
const
|
|
492
|
+
const resolved = entry.resolved ?? {};
|
|
493
|
+
const cachedTools = options.force ? [] : entry.slugs.filter((slug) => resolved[slug]).map((slug) => toolFromResolved(slug, resolved[slug], entry.cliCommand));
|
|
494
|
+
const unresolvedSlugs = options.force ? entry.slugs : entry.slugs.filter((slug) => !resolved[slug]);
|
|
495
|
+
const rawFreshTools = await fetchUnresolvedSlugs(entry, unresolvedSlugs);
|
|
496
|
+
const freshTools = entry.provider === 'composio' ? await withRealComposioDescriptions(rawFreshTools, unresolvedSlugs, entry.cliCommand) : rawFreshTools;
|
|
497
|
+
if (freshTools.length) {
|
|
498
|
+
const newlyResolved = {};
|
|
499
|
+
freshTools.forEach((tool, i) => {
|
|
500
|
+
newlyResolved[unresolvedSlugs[i]] = { name: tool.name, description: tool.description, input_schema: tool.input_schema };
|
|
501
|
+
});
|
|
502
|
+
persistResolvedTools(dir, entry.name, newlyResolved);
|
|
503
|
+
}
|
|
504
|
+
const tools = [...cachedTools, ...freshTools];
|
|
393
505
|
return { entry, status: 'ok', tools: tools.map((t) => ({ name: t.name, description: t.description })) };
|
|
394
506
|
}
|
|
395
507
|
catch (err) {
|
|
@@ -435,7 +547,7 @@ export async function loadGatewayToolsFromDir(dir) {
|
|
|
435
547
|
const entries = readGatewayToolsFromDir(dir);
|
|
436
548
|
const resolved = await Promise.all(entries.map(async (entry) => {
|
|
437
549
|
try {
|
|
438
|
-
return await connectEntry(entry);
|
|
550
|
+
return await connectEntry(entry, dir);
|
|
439
551
|
}
|
|
440
552
|
catch (err) {
|
|
441
553
|
console.error(`[gateway-tools] source '${entry.name}' (${entry.provider}) failed to load, skipping: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -542,7 +654,7 @@ export async function getComposioAuthStatus(cliCommand = 'composio') {
|
|
|
542
654
|
* Composio's own documented path is plain, interactive `composio login`
|
|
543
655
|
* (opens a browser) after installing the CLI — not something a web
|
|
544
656
|
* request can drive, so the Gateways panel shows that as instructions
|
|
545
|
-
* instead of a form; see its own comment in
|
|
657
|
+
* instead of a form; see its own comment in web/global-config-page.ts. */
|
|
546
658
|
export async function disconnectComposioAccount(cliCommand = 'composio') {
|
|
547
659
|
await execFileAsync(cliCommand, ['logout']);
|
|
548
660
|
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { CheckResult } from 'contextclip';
|
|
2
|
+
import type { RecoveryAction } from 'reflowkit';
|
|
3
|
+
import type { Decision, PendingApproval } from 'actauth';
|
|
4
|
+
import type { PendingQuestion } from './system-tools/index.js';
|
|
5
|
+
/** ContextClipper's own read-only budget check, run once per loop turn
|
|
6
|
+
* before the model call — reported unchanged (spread, not renamed) so
|
|
7
|
+
* this never drifts from whatever contextclip's own CheckResult actually
|
|
8
|
+
* contains. */
|
|
9
|
+
export type ContextClipCheckEvent = {
|
|
10
|
+
type: 'contextclip:check';
|
|
11
|
+
} & CheckResult;
|
|
12
|
+
/** ContextClip recovery kicked in because the prompt was rejected as too
|
|
13
|
+
* large — `from`/`to` are message-array lengths (this turn's own
|
|
14
|
+
* newMessages included), not token counts; see run-agent.ts's own
|
|
15
|
+
* reflow.call onPromptTooLong hook. */
|
|
16
|
+
export interface ReflowRecoverEvent {
|
|
17
|
+
type: 'reflow:recover';
|
|
18
|
+
from: number;
|
|
19
|
+
to: number;
|
|
20
|
+
}
|
|
21
|
+
/** Ordered log of whatever reflowkit's own Reflow.call had to do to get a
|
|
22
|
+
* usable response this turn (prompt-too-long retries, truncation
|
|
23
|
+
* retries, ...) — empty-array case never fires this event at all (see
|
|
24
|
+
* run-agent.ts: only logged when recoveries.length > 0). */
|
|
25
|
+
export interface ReflowRecoveriesEvent {
|
|
26
|
+
type: 'reflow:recoveries';
|
|
27
|
+
recoveries: RecoveryAction[];
|
|
28
|
+
}
|
|
29
|
+
/** The model's own "I'll do X" preamble alongside a tool_use request —
|
|
30
|
+
* the only *live* signal of assistant text mid-turn (the final answer
|
|
31
|
+
* only ever arrives via 'loop:done'/'done', once, at the very end). */
|
|
32
|
+
export interface AssistantTextEvent {
|
|
33
|
+
type: 'assistant:text';
|
|
34
|
+
text: string;
|
|
35
|
+
}
|
|
36
|
+
/** One ActAuth Gate.evaluate() outcome per requested tool call — 'allow'
|
|
37
|
+
* here can still mean a human was asked interactively and said yes (see
|
|
38
|
+
* run-agent.ts's own wasAskedInteractively); this event fires regardless,
|
|
39
|
+
* the interactive case just also gets its own approval:pending. */
|
|
40
|
+
export interface ActauthDecisionEvent {
|
|
41
|
+
type: 'actauth:decision';
|
|
42
|
+
tool: string;
|
|
43
|
+
decision: Decision;
|
|
44
|
+
reason: string;
|
|
45
|
+
}
|
|
46
|
+
/** A tool call's decision is in and it's about to run (or, for a system
|
|
47
|
+
* tool, already bypassed the gate entirely) — fired the instant that's
|
|
48
|
+
* known, not once execution finishes, so a live caller can show
|
|
49
|
+
* "Running…" immediately instead of a silent gap. Matched to its later
|
|
50
|
+
* 'tool:result' by `id` (the model's own tool_use id). */
|
|
51
|
+
export interface ToolStartedEvent {
|
|
52
|
+
type: 'tool:started';
|
|
53
|
+
id: string;
|
|
54
|
+
tool: string;
|
|
55
|
+
args: Record<string, unknown>;
|
|
56
|
+
detailText: string;
|
|
57
|
+
}
|
|
58
|
+
/** A tool call's resolution — auto-allowed, interactively approved,
|
|
59
|
+
* denied, or skipped alike. `args`/`detailText` are omitted for some
|
|
60
|
+
* skipped/denied paths that never had them resolved (see run-agent.ts's
|
|
61
|
+
* own call sites). */
|
|
62
|
+
export interface ToolResultEvent {
|
|
63
|
+
type: 'tool:result';
|
|
64
|
+
id: string;
|
|
65
|
+
tool: string;
|
|
66
|
+
args?: Record<string, unknown>;
|
|
67
|
+
detailText?: string;
|
|
68
|
+
statusText: string;
|
|
69
|
+
}
|
|
70
|
+
/** ToolLane's own raw fulfil/reject outcome, logged alongside (not
|
|
71
|
+
* instead of) the richer 'tool:result' above — kept as its own event
|
|
72
|
+
* since it's toollane's own vocabulary (`summary`, not `statusText`), not
|
|
73
|
+
* reshaped to match. */
|
|
74
|
+
export interface ToolLaneResultEvent {
|
|
75
|
+
type: 'toollane:result';
|
|
76
|
+
name: string;
|
|
77
|
+
summary: string;
|
|
78
|
+
}
|
|
79
|
+
/** A `Skill` tool_use block was resolved to a real skill body via
|
|
80
|
+
* SkillGarden — not gated by ActAuth (see run-agent.ts's own comment on
|
|
81
|
+
* why Skill invocation isn't a real tool call). */
|
|
82
|
+
export interface SkillGardenInvokeEvent {
|
|
83
|
+
type: 'skillgarden:invoke';
|
|
84
|
+
skill: string;
|
|
85
|
+
}
|
|
86
|
+
/** An approved call never ran because a sibling call in the same turn's
|
|
87
|
+
* batch was denied — the whole batch cancels together (see
|
|
88
|
+
* RunAgentResult.stopReason's own doc comment). */
|
|
89
|
+
export interface LoopSkippedEvent {
|
|
90
|
+
type: 'loop:skipped';
|
|
91
|
+
name: string;
|
|
92
|
+
deniedTools: string[];
|
|
93
|
+
}
|
|
94
|
+
/** Hit config.maxTurns without the model producing a final answer. */
|
|
95
|
+
export interface LoopMaxTurnsEvent {
|
|
96
|
+
type: 'loop:max_turns';
|
|
97
|
+
maxTurns: number;
|
|
98
|
+
}
|
|
99
|
+
/** A human denied at least one requested tool call this turn — the whole
|
|
100
|
+
* turn stops right there (see RunAgentResult.stopReason). */
|
|
101
|
+
export interface LoopDeniedEvent {
|
|
102
|
+
type: 'loop:denied';
|
|
103
|
+
deniedTools: string[];
|
|
104
|
+
}
|
|
105
|
+
/** The loop's own final answer — the model produced text with no more
|
|
106
|
+
* tool_use blocks. Distinct from the adapter-level 'done' below: this
|
|
107
|
+
* only ever means a genuine finish, never a synthetic max_turns/denied
|
|
108
|
+
* notice (see adapters/http.ts's own 'done', which covers all three). */
|
|
109
|
+
export interface LoopDoneEvent {
|
|
110
|
+
type: 'loop:done';
|
|
111
|
+
text: string;
|
|
112
|
+
}
|
|
113
|
+
/** Every event runAgent() itself can emit via RunAgentOptions.onEvent —
|
|
114
|
+
* not the adapter-synthesized ones below, which no caller of runAgent()
|
|
115
|
+
* itself ever produces. */
|
|
116
|
+
export type RunAgentLoopEvent = ContextClipCheckEvent | ReflowRecoverEvent | ReflowRecoveriesEvent | AssistantTextEvent | ActauthDecisionEvent | ToolStartedEvent | ToolResultEvent | ToolLaneResultEvent | SkillGardenInvokeEvent | LoopSkippedEvent | LoopMaxTurnsEvent | LoopDeniedEvent | LoopDoneEvent;
|
|
117
|
+
/** adapters/http.ts echoing back which session id this turn used — first
|
|
118
|
+
* event of every streamed turn, so a caller that omitted sessionId can
|
|
119
|
+
* capture what got generated. Not something runAgent() itself knows
|
|
120
|
+
* (session ids are resolved per-request by the adapter, before runAgent
|
|
121
|
+
* is even called). */
|
|
122
|
+
export interface SessionEvent {
|
|
123
|
+
type: 'session';
|
|
124
|
+
sessionId: string;
|
|
125
|
+
}
|
|
126
|
+
/** A tool call needs a human 'ask' decision, live — spreads actauth's own
|
|
127
|
+
* PendingApproval verbatim (id/tool/args/scope/reason/requestedAt)
|
|
128
|
+
* alongside `type`, so this never drifts from whatever that package
|
|
129
|
+
* actually returns. */
|
|
130
|
+
export type ApprovalPendingEvent = {
|
|
131
|
+
type: 'approval:pending';
|
|
132
|
+
} & PendingApproval;
|
|
133
|
+
/** The system ask_user tool raised a genuinely ambiguous question — same
|
|
134
|
+
* spread-verbatim reasoning as ApprovalPendingEvent above, against
|
|
135
|
+
* system-tools/ask_user.ts's own PendingQuestion. */
|
|
136
|
+
export type QuestionPendingEvent = {
|
|
137
|
+
type: 'question:pending';
|
|
138
|
+
} & PendingQuestion;
|
|
139
|
+
/** The turn is over — covers a genuine finish and both synthetic
|
|
140
|
+
* stopReasons (max_turns/denied) alike, the one event every caller can
|
|
141
|
+
* treat as "nothing more is coming" regardless of which of the three it
|
|
142
|
+
* was (see RunAgentResult.stopReason's own doc comment). */
|
|
143
|
+
export interface DoneEvent {
|
|
144
|
+
type: 'done';
|
|
145
|
+
text: string;
|
|
146
|
+
stopReason?: 'max_turns' | 'denied';
|
|
147
|
+
}
|
|
148
|
+
/** Something failed after the response was already committed (SSE
|
|
149
|
+
* headers sent, or a background turn already running) — surfaced in-band
|
|
150
|
+
* rather than as an HTTP status code, since it's too late for one. */
|
|
151
|
+
export interface ErrorEvent {
|
|
152
|
+
type: 'error';
|
|
153
|
+
error: string;
|
|
154
|
+
}
|
|
155
|
+
/** Synthesized by adapters/http.ts around a turn — session ids and
|
|
156
|
+
* HTTP-level errors aren't things runAgent() itself produces, but they're
|
|
157
|
+
* still part of the same lifecycle a caller observes end to end. */
|
|
158
|
+
export type AdapterLoopEvent = SessionEvent | ApprovalPendingEvent | QuestionPendingEvent | DoneEvent | ErrorEvent;
|
|
159
|
+
/** The full wire vocabulary — what run-agent.ts emits via onEvent, plus
|
|
160
|
+
* what adapters/http.ts (and, in --json mode, adapters/cli.ts) layers
|
|
161
|
+
* around it. This is "the standard output" every channel adapter is a
|
|
162
|
+
* projection of: adapters/http.ts's SSE stream writes one of these per
|
|
163
|
+
* frame; its plain /messages route collects the whole array into an
|
|
164
|
+
* `events` response field; adapters/cli.ts's --json mode writes one per
|
|
165
|
+
* NDJSON line; web/playground.ts is just a UI rendering whichever of
|
|
166
|
+
* those two HTTP routes it's talking to. */
|
|
167
|
+
export type LoopEvent = RunAgentLoopEvent | AdapterLoopEvent;
|
|
168
|
+
export type LoopEventType = LoopEvent['type'];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Anthropic from '@anthropic-ai/sdk';
|
|
2
|
-
import type { ModelCall } from '#run-agent.js';
|
|
2
|
+
import type { ModelCall } from '#core/run-agent.js';
|
|
3
3
|
export interface AnthropicModelCallOptions {
|
|
4
4
|
/** Defaults to the ANTHROPIC_API_KEY env var, same as the SDK itself. */
|
|
5
5
|
apiKey?: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import OpenAI from 'openai';
|
|
2
|
-
import type { Message, ModelCall, ModelResponse } from '#run-agent.js';
|
|
2
|
+
import type { Message, ModelCall, ModelResponse } from '#core/run-agent.js';
|
|
3
3
|
type ChatMessage = OpenAI.Chat.Completions.ChatCompletionMessageParam;
|
|
4
4
|
export interface OpenAIModelCallOptions {
|
|
5
5
|
/** Defaults to the OPENAI_API_KEY env var, same as the SDK itself. */
|