loopengine 0.1.59 → 0.1.60

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.
@@ -26,6 +26,17 @@ export interface AbilityEnvDecl {
26
26
  * env-admin.ts's own serializeEnvValue doc comment). Omit for anything
27
27
  * genuinely single-line. */
28
28
  multiline?: boolean;
29
+ /** True when this setting is meaningfully different per agent (e.g.
30
+ * which Slack channel to post to) rather than one real shared
31
+ * credential every installed copy should agree on (e.g. an API key
32
+ * for the one external service they all call). The Admin UI's
33
+ * Environment tab then also offers a per-agent override — a second,
34
+ * separate var named `<AGENT_NAME>_<this name>` (env-admin.ts's own
35
+ * agentScopedEnvVarName) — that an ability's own tool code can check
36
+ * first, falling back to this bare name when the override isn't set.
37
+ * Omit (the default, false) for anything that's actually fine shared
38
+ * across every agent that installs this ability. */
39
+ perAgent?: boolean;
29
40
  }
30
41
  /** `name`/`version` deliberately aren't declared here — they're read off
31
42
  * the ability's own sibling `package.json` instead (see readManifest),
@@ -35,6 +35,25 @@ export type GatewayToolEntry = ComposioGatewayToolEntry;
35
35
  * module's own registry read/writes do, rather than re-deriving it (e.g.
36
36
  * from process.cwd()) and risking the two drifting apart. */
37
37
  export declare function agentDir(agentName: string): string;
38
+ /** agentDir's own inverse, for a tool file that needs to know which
39
+ * agent it was installed under without an env var to tell it — an
40
+ * ability's own tool file (copied to agents/<name>/tools/<file>.ts by
41
+ * add-ability) calls this with its own `import.meta.url`, and gets
42
+ * `<name>` back, by walking back up from the file's real on-disk
43
+ * location rather than trusting anything passed in (there's nothing
44
+ * else to trust: this runs from inside the tool file itself, before
45
+ * any agent-scoped state exists). Returns undefined for a tool file
46
+ * that isn't actually at that expected agents/<name>/tools/ depth (a
47
+ * test harness loading the file from somewhere else, say) — a caller
48
+ * needing a default when this can't be determined should fall back
49
+ * explicitly, not treat undefined as a crash. Mirrors the inferAgentName
50
+ * helper lp-task-scheduler's own tool files (schedule_task.ts et al.)
51
+ * already hand-duplicate per the ability system's own "no shared
52
+ * modules between tool files" rule — this is the one place that's
53
+ * actually safe to centralize, since every ability's tool file already
54
+ * imports real functions from this package (see e.g. lp-task-scheduler's
55
+ * own createCheckpointStore/runAgent imports), just not yet this. */
56
+ export declare function inferAgentNameFromToolUrl(toolFileUrl: string): string | undefined;
38
57
  /** `dir` is the agent's real folder — `agents/<name>` for a top-level
39
58
  * agent, or the nested `agents/<parent>/subagents/<child>` a subagent
40
59
  * actually lives in (see run-agent.ts's resolveSubagentConfig, which
@@ -8,7 +8,8 @@
8
8
  // the only intended writer of that file; loadGatewayToolsFromDir is what
9
9
  // run-agent.ts calls to actually resolve it into ToolDefinitions.
10
10
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
11
- import { join } from 'node:path';
11
+ import { basename, dirname, join } from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
12
13
  import { execFile } from 'node:child_process';
13
14
  import { promisify } from 'node:util';
14
15
  import { parse as parseYaml, parseDocument, stringify as stringifyYaml } from 'yaml';
@@ -34,6 +35,36 @@ function gatewayToolsPath(dir) {
34
35
  export function agentDir(agentName) {
35
36
  return join(agentsRootDir, agentName);
36
37
  }
38
+ /** agentDir's own inverse, for a tool file that needs to know which
39
+ * agent it was installed under without an env var to tell it — an
40
+ * ability's own tool file (copied to agents/<name>/tools/<file>.ts by
41
+ * add-ability) calls this with its own `import.meta.url`, and gets
42
+ * `<name>` back, by walking back up from the file's real on-disk
43
+ * location rather than trusting anything passed in (there's nothing
44
+ * else to trust: this runs from inside the tool file itself, before
45
+ * any agent-scoped state exists). Returns undefined for a tool file
46
+ * that isn't actually at that expected agents/<name>/tools/ depth (a
47
+ * test harness loading the file from somewhere else, say) — a caller
48
+ * needing a default when this can't be determined should fall back
49
+ * explicitly, not treat undefined as a crash. Mirrors the inferAgentName
50
+ * helper lp-task-scheduler's own tool files (schedule_task.ts et al.)
51
+ * already hand-duplicate per the ability system's own "no shared
52
+ * modules between tool files" rule — this is the one place that's
53
+ * actually safe to centralize, since every ability's tool file already
54
+ * imports real functions from this package (see e.g. lp-task-scheduler's
55
+ * own createCheckpointStore/runAgent imports), just not yet this. */
56
+ export function inferAgentNameFromToolUrl(toolFileUrl) {
57
+ try {
58
+ const toolsDir = dirname(fileURLToPath(toolFileUrl));
59
+ const agentDir = dirname(toolsDir);
60
+ if (basename(dirname(agentDir)) !== 'agents')
61
+ return undefined;
62
+ return basename(agentDir);
63
+ }
64
+ catch {
65
+ return undefined;
66
+ }
67
+ }
37
68
  /** `dir` is the agent's real folder — `agents/<name>` for a top-level
38
69
  * agent, or the nested `agents/<parent>/subagents/<child>` a subagent
39
70
  * actually lives in (see run-agent.ts's resolveSubagentConfig, which
@@ -0,0 +1,25 @@
1
+ ---
2
+ name: composio-large-outputs
3
+ description: How to retrieve a gateway tool's real output when its result says storedInFile is true instead of returning the data inline.
4
+ ---
5
+
6
+ # Gateway tools with a large result
7
+
8
+ Some gateway tools (Composio-sourced ones in particular) return a small
9
+ pointer object instead of their real output when the actual result is
10
+ too large to return inline:
11
+
12
+ ```
13
+ {
14
+ "successful": true,
15
+ "storedInFile": true,
16
+ "tokenCount": 10234,
17
+ "outputFilePath": "/tmp/.../SOME_TOOL_OUTPUT_xxxxx.json"
18
+ }
19
+ ```
20
+
21
+ When you see `storedInFile: true` in a tool result, the real data is
22
+ **not** in that object — it's in the file at `outputFilePath`. Call
23
+ `system_read_file` with that exact path to retrieve the actual content
24
+ before answering. Don't guess at an answer, summarize from the pointer
25
+ alone, or claim you don't have the data — read the file first.
package/dist/index.d.ts CHANGED
@@ -36,7 +36,7 @@ export { LLMSummarizer } from './core/llm-summarizer.js';
36
36
  export type { LLMSummarizerOptions } from './core/llm-summarizer.js';
37
37
  export { createCheckpointStore } from './core/durable-approvals.js';
38
38
  export type { TurnCheckpoint, CheckpointStore, OutstandingItem } from './core/durable-approvals.js';
39
- export { addGatewayTool, agentDir, disconnectComposioAccount, describeGatewayTools, listComposioConnections, listComposioTools, loadGatewayToolsFromDir, removeGatewayTool, removeGatewayToolSlug, GatewayToolExistsError, GatewayToolNotFoundError, } from './core/gateway-tools.js';
39
+ export { addGatewayTool, agentDir, inferAgentNameFromToolUrl, disconnectComposioAccount, describeGatewayTools, listComposioConnections, listComposioTools, loadGatewayToolsFromDir, removeGatewayTool, removeGatewayToolSlug, GatewayToolExistsError, GatewayToolNotFoundError, } from './core/gateway-tools.js';
40
40
  export type { GatewayToolEntry, GatewayToolDecision } from './core/gateway-tools.js';
41
41
  export { connectComposioSource } from './core/mcpplug.js';
42
42
  export type { ToolSource, ComposioSourceOptions } from './core/mcpplug.js';
@@ -55,7 +55,7 @@ export { globalConfigPageHtml } from './web/global-config-page.js';
55
55
  export { playgroundHtml } from './web/playground.js';
56
56
  export { readSkill, writeSkill, deleteSkill, SkillInvalidIdError, SkillNotFoundError } from './web/skills-admin.js';
57
57
  export type { SkillContent } from './web/skills-admin.js';
58
- export { listDeclaredEnvVars, setEnvVar, unsetEnvVar, EnvVarNameError } from './web/env-admin.js';
58
+ export { listDeclaredEnvVars, setEnvVar, unsetEnvVar, agentScopedEnvVarName, EnvVarNameError } from './web/env-admin.js';
59
59
  export type { DeclaredEnvVar } from './web/env-admin.js';
60
60
  export { readActauthConfig, addActauthRule, updateActauthRule, removeActauthRule, setDefaultDecision, ActauthRuleExistsError, ActauthRuleNotFoundError, } from './web/actauth-admin.js';
61
61
  export type { ActauthRuleInput, ActauthConfigView } from './web/actauth-admin.js';
package/dist/index.js CHANGED
@@ -35,7 +35,7 @@ export { resumeAgent, loadRules, loadDefaultTools, loadSubagentAsTools, systemTo
35
35
  export { Compactor, TruncatingSummarizer } from './core/compaction.js';
36
36
  export { LLMSummarizer } from './core/llm-summarizer.js';
37
37
  export { createCheckpointStore } from './core/durable-approvals.js';
38
- export { addGatewayTool, agentDir, disconnectComposioAccount, describeGatewayTools, listComposioConnections, listComposioTools, loadGatewayToolsFromDir, removeGatewayTool, removeGatewayToolSlug, GatewayToolExistsError, GatewayToolNotFoundError, } from './core/gateway-tools.js';
38
+ export { addGatewayTool, agentDir, inferAgentNameFromToolUrl, disconnectComposioAccount, describeGatewayTools, listComposioConnections, listComposioTools, loadGatewayToolsFromDir, removeGatewayTool, removeGatewayToolSlug, GatewayToolExistsError, GatewayToolNotFoundError, } from './core/gateway-tools.js';
39
39
  export { connectComposioSource } from './core/mcpplug.js';
40
40
  export { ToolLane, buildLanes, runLanes } from './core/toollane.js';
41
41
  export { WebhookNotifier } from './core/http-notify-triggers/webhook.js';
@@ -50,7 +50,7 @@ export { agentsListPageHtml } from './web/agents-list-page.js';
50
50
  export { globalConfigPageHtml } from './web/global-config-page.js';
51
51
  export { playgroundHtml } from './web/playground.js';
52
52
  export { readSkill, writeSkill, deleteSkill, SkillInvalidIdError, SkillNotFoundError } from './web/skills-admin.js';
53
- export { listDeclaredEnvVars, setEnvVar, unsetEnvVar, EnvVarNameError } from './web/env-admin.js';
53
+ export { listDeclaredEnvVars, setEnvVar, unsetEnvVar, agentScopedEnvVarName, EnvVarNameError } from './web/env-admin.js';
54
54
  export { readActauthConfig, addActauthRule, updateActauthRule, removeActauthRule, setDefaultDecision, ActauthRuleExistsError, ActauthRuleNotFoundError, } from './web/actauth-admin.js';
55
55
  export { installAbility, upgradeAbility, listInstalledAbilities, backfillDependencies, AbilityManifestError, AbilityVersionError, AbilityCollisionError, AbilityAlreadyInstalledError, AbilityNotInstalledError, } from './bin/ability-manager.js';
56
56
  export { searchPublicAbilities, fetchLatestAbilityVersion } from './web/abilities-admin.js';
@@ -48,6 +48,16 @@ export interface DeclaredEnvVar {
48
48
  * live off `process.env`, not cached, so it reflects whatever the last
49
49
  * `setEnvVar` call — or a plain restart picking up `.env` — actually
50
50
  * did. */
51
+ /** `varName`'s own per-agent override name (see AbilityEnvDecl.perAgent)
52
+ * — `<AGENT_NAME>_<varName>`, with `agentName` upper-cased and anything
53
+ * that isn't `[A-Z0-9_]` (a hyphen, most likely — agent directory names
54
+ * are kebab-case) turned into `_`, same alphabet ENV_VAR_NAME_PATTERN
55
+ * already requires. Guards against a result starting with a digit (a
56
+ * from-digits agent name, or an agent name that's entirely punctuation)
57
+ * the same way that pattern would otherwise reject — by prefixing one
58
+ * more `_` — though in practice agent names are never actually bare
59
+ * digits. */
60
+ export declare function agentScopedEnvVarName(agentName: string, varName: string): string;
51
61
  export declare function listDeclaredEnvVars(agentName: string): DeclaredEnvVar[];
52
62
  /** Upserts `NAME=VALUE` into the project's `.env` file — preserving
53
63
  * every other line (comments, blank lines, unrelated keys) — and
@@ -41,33 +41,61 @@ function provenancePath(agentName) {
41
41
  * live off `process.env`, not cached, so it reflects whatever the last
42
42
  * `setEnvVar` call — or a plain restart picking up `.env` — actually
43
43
  * did. */
44
+ /** `varName`'s own per-agent override name (see AbilityEnvDecl.perAgent)
45
+ * — `<AGENT_NAME>_<varName>`, with `agentName` upper-cased and anything
46
+ * that isn't `[A-Z0-9_]` (a hyphen, most likely — agent directory names
47
+ * are kebab-case) turned into `_`, same alphabet ENV_VAR_NAME_PATTERN
48
+ * already requires. Guards against a result starting with a digit (a
49
+ * from-digits agent name, or an agent name that's entirely punctuation)
50
+ * the same way that pattern would otherwise reject — by prefixing one
51
+ * more `_` — though in practice agent names are never actually bare
52
+ * digits. */
53
+ export function agentScopedEnvVarName(agentName, varName) {
54
+ const prefix = agentName.toUpperCase().replace(/[^A-Z0-9_]/g, '_');
55
+ return `${/^[0-9]/.test(prefix) ? '_' : ''}${prefix}_${varName}`;
56
+ }
44
57
  export function listDeclaredEnvVars(agentName) {
45
58
  const path = provenancePath(agentName);
46
59
  if (!existsSync(path))
47
60
  return [];
48
61
  const provenance = JSON.parse(readFileSync(path, 'utf8'));
49
62
  const byName = new Map();
63
+ function upsert(name, abilityName, decl, description) {
64
+ const existing = byName.get(name);
65
+ if (existing) {
66
+ existing.abilityNames.push(abilityName);
67
+ existing.secret = existing.secret || decl.secret === true;
68
+ if (existing.secret)
69
+ existing.value = undefined;
70
+ return;
71
+ }
72
+ const rawValue = process.env[name];
73
+ byName.set(name, {
74
+ name,
75
+ description,
76
+ secret: decl.secret === true,
77
+ abilityNames: [abilityName],
78
+ set: rawValue !== undefined,
79
+ value: decl.secret === true ? undefined : rawValue,
80
+ options: decl.options,
81
+ multiline: decl.multiline,
82
+ });
83
+ }
50
84
  for (const [abilityName, record] of Object.entries(provenance)) {
51
85
  for (const decl of record.env) {
52
- const existing = byName.get(decl.name);
53
- if (existing) {
54
- existing.abilityNames.push(abilityName);
55
- existing.secret = existing.secret || decl.secret === true;
56
- if (existing.secret)
57
- existing.value = undefined;
58
- continue;
86
+ upsert(decl.name, abilityName, decl, decl.description);
87
+ // A perAgent var also gets its own separate, independently
88
+ // set/unset row for this one agent's override — see
89
+ // AbilityEnvDecl.perAgent's own doc comment for why this is a
90
+ // second row, not a replacement for the shared one above (an
91
+ // ability's own tool code checks the override first and falls
92
+ // back to the shared value, so both need to stay independently
93
+ // visible and settable here).
94
+ if (decl.perAgent) {
95
+ const scopedName = agentScopedEnvVarName(agentName, decl.name);
96
+ const overrideDescription = `Overrides ${decl.name} for this agent only — if unset, falls back to the shared value above.${decl.description ? ` ${decl.description}` : ''}`;
97
+ upsert(scopedName, abilityName, decl, overrideDescription);
59
98
  }
60
- const rawValue = process.env[decl.name];
61
- byName.set(decl.name, {
62
- name: decl.name,
63
- description: decl.description,
64
- secret: decl.secret === true,
65
- abilityNames: [abilityName],
66
- set: rawValue !== undefined,
67
- value: decl.secret === true ? undefined : rawValue,
68
- options: decl.options,
69
- multiline: decl.multiline,
70
- });
71
99
  }
72
100
  }
73
101
  return [...byName.values()];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopengine",
3
- "version": "0.1.59",
3
+ "version": "0.1.60",
4
4
  "type": "module",
5
5
  "description": "Runtime for tool-using AI agents with a transparent ReAct loop.",
6
6
  "license": "MIT",