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.
Files changed (38) hide show
  1. package/README.md +34 -3
  2. package/core/system-skills/composio-large-outputs/SKILL.md +25 -0
  3. package/dist/{cli.d.ts → bin/cli.d.ts} +25 -4
  4. package/dist/{cli.js → bin/cli.js} +131 -16
  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} +11 -3
  11. package/dist/{gateway-tools.d.ts → core/gateway-tools.d.ts} +38 -6
  12. package/dist/{gateway-tools.js → core/gateway-tools.js} +140 -28
  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/{run-agent.d.ts → core/run-agent.d.ts} +77 -9
  19. package/dist/{run-agent.js → core/run-agent.js} +213 -44
  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 +32 -26
  31. package/dist/discover-agents.d.ts +0 -24
  32. /package/dist/{agent-config.d.ts → core/agent-config.d.ts} +0 -0
  33. /package/dist/{agent-config.js → core/agent-config.js} +0 -0
  34. /package/dist/{model-calls → core/model-calls}/anthropic-model-call.js +0 -0
  35. /package/dist/{model-calls → core/model-calls}/deepseek-model-call.js +0 -0
  36. /package/dist/{model-calls → core/model-calls}/openai-model-call.js +0 -0
  37. /package/dist/{vector-index.d.ts → core/vector-index.d.ts} +0 -0
  38. /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
- 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');
@@ -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. Deliberately bypasses
107
- * loadGatewayToolsFromDir's mtime cache — this is an occasional,
108
- * deliberate operator action, not a per-turn hot path, so "always
109
- * current" is worth more here than "cheap." */
110
- export declare function describeGatewayTools(agentName: string): Promise<GatewayToolStatus[]>;
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 adapters/global-config-page.ts. */
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 "next to this file, not process.cwd()" reasoning run-agent.ts's
19
- // own agentsRootDir uses — real code (an actauth.yml write, a
20
- // gateway-tools.yml write) needs to land in the same place a built dist/
21
- // would look for it, not wherever the process happened to be launched
22
- // from.
23
- const agentsRootDir = join(dirname(fileURLToPath(import.meta.url)), 'agents');
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) => (i === index ? { ...s, slugs: remainingSlugs } : s));
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
- async function connectEntry(entry) {
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 source = await connectComposioSource(entry.name, { slugs: entry.slugs, cliCommand: entry.cliCommand });
316
- try {
317
- return await source.loadTools();
318
- }
319
- finally {
320
- await source.close();
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 (see its own "always current worth
338
- * more than cheap" doc comment), an occasional, deliberate admin action.
339
- * Deliberately *not* applied inside connectEntry itself, even though
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. Deliberately bypasses
382
- * loadGatewayToolsFromDir's mtime cache — this is an occasional,
383
- * deliberate operator action, not a per-turn hot path, so "always
384
- * current" is worth more here than "cheap." */
385
- export async function describeGatewayTools(agentName) {
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 rawTools = await connectEntry(entry);
390
- // See withRealComposioDescriptions' own doc comment for why this
391
- // enrichment happens here, not inside connectEntry itself.
392
- const tools = entry.provider === 'composio' ? await withRealComposioDescriptions(rawTools, entry.slugs, entry.cliCommand) : rawTools;
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 adapters/global-config-page.ts. */
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 { ModelCall } from '#run-agent.js';
2
+ import type { ModelCall } from '#core/run-agent.js';
3
3
  export interface DeepSeekModelCallOptions {
4
4
  /** Defaults to the DEEPSEEK_API_KEY env var. */
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. */