@ethosagent/core 0.4.2 → 0.4.3

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/dist/index.js CHANGED
@@ -355,6 +355,7 @@ import { createHash } from "crypto";
355
355
  import { join, resolve, sep } from "path";
356
356
 
357
357
  // ../storage-fs/src/fs-storage.ts
358
+ import { existsSync as fsExistsSync } from "fs";
358
359
  import {
359
360
  appendFile,
360
361
  chmod,
@@ -664,6 +665,26 @@ var DefaultContextEngineRegistry = class {
664
665
  }
665
666
  };
666
667
 
668
+ // src/context-store.ts
669
+ var ContextStore = class {
670
+ store = /* @__PURE__ */ new Map();
671
+ get(key) {
672
+ return this.store.get(key);
673
+ }
674
+ set(key, value) {
675
+ this.store.set(key, value);
676
+ }
677
+ clear() {
678
+ this.store.clear();
679
+ }
680
+ asContextMethods() {
681
+ return {
682
+ getContext: (key) => this.get(key),
683
+ setContext: (key, value) => this.set(key, value)
684
+ };
685
+ }
686
+ };
687
+
667
688
  // src/defaults/in-memory-session.ts
668
689
  var InMemorySessionStore = class {
669
690
  sessions = /* @__PURE__ */ new Map();
@@ -972,6 +993,101 @@ var DefaultHookRegistry = class {
972
993
  }
973
994
  };
974
995
 
996
+ // src/simple-completion.ts
997
+ var SimpleCompletionImpl = class {
998
+ constructor(provider, defaultModel, onUsage) {
999
+ this.provider = provider;
1000
+ this.defaultModel = defaultModel;
1001
+ this.onUsage = onUsage;
1002
+ }
1003
+ provider;
1004
+ defaultModel;
1005
+ onUsage;
1006
+ async complete(prompt, options) {
1007
+ const model = options?.model ?? this.defaultModel;
1008
+ let text = "";
1009
+ let inputTokens = 0;
1010
+ let outputTokens = 0;
1011
+ const stream = this.provider.complete([{ role: "user", content: prompt }], [], {
1012
+ system: options?.systemPrompt,
1013
+ maxTokens: options?.maxTokens ?? 1024,
1014
+ modelOverride: model !== this.provider.model ? model : void 0
1015
+ });
1016
+ for await (const chunk of stream) {
1017
+ if (chunk.type === "text_delta") text += chunk.text;
1018
+ if (chunk.type === "usage") {
1019
+ inputTokens += chunk.usage.inputTokens;
1020
+ outputTokens += chunk.usage.outputTokens;
1021
+ }
1022
+ }
1023
+ this.onUsage({ input: inputTokens, output: outputTokens });
1024
+ return text;
1025
+ }
1026
+ };
1027
+
1028
+ // src/capability-validator.ts
1029
+ function hostMatchesPattern(host, pattern) {
1030
+ if (pattern === host) return true;
1031
+ if (pattern === "*") return true;
1032
+ if (pattern.startsWith("*.")) {
1033
+ const suffix = pattern.slice(1);
1034
+ return host.endsWith(suffix) && host.length > suffix.length;
1035
+ }
1036
+ return false;
1037
+ }
1038
+ function validateRegistration(tool, personality) {
1039
+ const caps = tool.capabilities;
1040
+ if (!caps) return [];
1041
+ const errors = [];
1042
+ if (caps.network) {
1043
+ const allowed = personality.safety?.network?.allow;
1044
+ if (allowed && allowed.length > 0) {
1045
+ for (const host of caps.network.allowedHosts) {
1046
+ if (host === "*") continue;
1047
+ const covered = allowed.some((pattern) => hostMatchesPattern(host, pattern));
1048
+ if (!covered) {
1049
+ errors.push({
1050
+ tool: tool.name,
1051
+ capability: "network",
1052
+ message: `host "${host}" is not in personality network allow list`
1053
+ });
1054
+ }
1055
+ }
1056
+ }
1057
+ }
1058
+ if (caps.fs_reach) {
1059
+ const personalityRead = personality.fs_reach?.read ?? [];
1060
+ const personalityWrite = personality.fs_reach?.write ?? [];
1061
+ if (caps.fs_reach.read && caps.fs_reach.read !== "from-personality") {
1062
+ for (const toolPath of caps.fs_reach.read) {
1063
+ const covered = personalityRead.some((p) => toolPath === p || toolPath.startsWith(`${p}/`));
1064
+ if (!covered) {
1065
+ errors.push({
1066
+ tool: tool.name,
1067
+ capability: "fs_reach.read",
1068
+ message: `path "${toolPath}" is not covered by personality fs_reach.read`
1069
+ });
1070
+ }
1071
+ }
1072
+ }
1073
+ if (caps.fs_reach.write && caps.fs_reach.write !== "from-personality") {
1074
+ for (const toolPath of caps.fs_reach.write) {
1075
+ const covered = personalityWrite.some(
1076
+ (p) => toolPath === p || toolPath.startsWith(`${p}/`)
1077
+ );
1078
+ if (!covered) {
1079
+ errors.push({
1080
+ tool: tool.name,
1081
+ capability: "fs_reach.write",
1082
+ message: `path "${toolPath}" is not covered by personality fs_reach.write`
1083
+ });
1084
+ }
1085
+ }
1086
+ }
1087
+ }
1088
+ return errors;
1089
+ }
1090
+
975
1091
  // src/scoped/scoped-attachments.ts
976
1092
  var ScopedAttachmentsImpl = class {
977
1093
  attachments;
@@ -1518,68 +1634,55 @@ function resolveCapabilities(toolName, capabilities, scopeIds, backends) {
1518
1634
  return result;
1519
1635
  }
1520
1636
 
1521
- // src/capability-validator.ts
1522
- function hostMatchesPattern(host, pattern) {
1523
- if (pattern === host) return true;
1524
- if (pattern === "*") return true;
1525
- if (pattern.startsWith("*.")) {
1526
- const suffix = pattern.slice(1);
1527
- return host.endsWith(suffix) && host.length > suffix.length;
1528
- }
1529
- return false;
1530
- }
1531
- function validateRegistration(tool, personality) {
1532
- const caps = tool.capabilities;
1533
- if (!caps) return [];
1534
- const errors = [];
1535
- if (caps.network) {
1536
- const allowed = personality.safety?.network?.allow;
1537
- if (allowed && allowed.length > 0) {
1538
- for (const host of caps.network.allowedHosts) {
1539
- if (host === "*") continue;
1540
- const covered = allowed.some((pattern) => hostMatchesPattern(host, pattern));
1541
- if (!covered) {
1542
- errors.push({
1543
- tool: tool.name,
1544
- capability: "network",
1545
- message: `host "${host}" is not in personality network allow list`
1546
- });
1547
- }
1548
- }
1549
- }
1637
+ // src/local-tool-transport.ts
1638
+ var LocalToolTransport = class {
1639
+ constructor(lookup, backends, getLiveCtx) {
1640
+ this.lookup = lookup;
1641
+ this.backends = backends;
1642
+ this.getLiveCtx = getLiveCtx;
1550
1643
  }
1551
- if (caps.fs_reach) {
1552
- const personalityRead = personality.fs_reach?.read ?? [];
1553
- const personalityWrite = personality.fs_reach?.write ?? [];
1554
- if (caps.fs_reach.read && caps.fs_reach.read !== "from-personality") {
1555
- for (const toolPath of caps.fs_reach.read) {
1556
- const covered = personalityRead.some((p) => toolPath === p || toolPath.startsWith(`${p}/`));
1557
- if (!covered) {
1558
- errors.push({
1559
- tool: tool.name,
1560
- capability: "fs_reach.read",
1561
- message: `path "${toolPath}" is not covered by personality fs_reach.read`
1562
- });
1563
- }
1564
- }
1644
+ lookup;
1645
+ backends;
1646
+ getLiveCtx;
1647
+ async execute(request, signal) {
1648
+ const tool = this.lookup(request.name);
1649
+ if (!tool) {
1650
+ return { ok: false, error: `Tool '${request.name}' not found`, code: "not_available" };
1565
1651
  }
1566
- if (caps.fs_reach.write && caps.fs_reach.write !== "from-personality") {
1567
- for (const toolPath of caps.fs_reach.write) {
1568
- const covered = personalityWrite.some(
1569
- (p) => toolPath === p || toolPath.startsWith(`${p}/`)
1570
- );
1571
- if (!covered) {
1572
- errors.push({
1573
- tool: tool.name,
1574
- capability: "fs_reach.write",
1575
- message: `path "${toolPath}" is not covered by personality fs_reach.write`
1576
- });
1577
- }
1578
- }
1652
+ const live = this.getLiveCtx?.();
1653
+ const ctx = {
1654
+ sessionId: request.sessionId,
1655
+ sessionKey: request.sessionKey,
1656
+ platform: request.platform,
1657
+ workingDir: request.workingDir,
1658
+ personalityId: request.personalityId,
1659
+ teamId: request.teamId,
1660
+ agentId: request.agentId,
1661
+ memoryScopeId: request.memoryScopeId,
1662
+ userScopeId: request.userScopeId,
1663
+ currentTurn: request.currentTurn,
1664
+ messageCount: request.messageCount,
1665
+ resultBudgetChars: request.resultBudgetChars,
1666
+ networkPolicy: request.networkPolicy,
1667
+ dryRun: request.dryRun,
1668
+ abortSignal: signal,
1669
+ emit: live?.emit ?? (() => {
1670
+ }),
1671
+ readMtimes: live?.readMtimes,
1672
+ storage: live?.storage
1673
+ };
1674
+ if (tool.capabilities && this.backends) {
1675
+ const resolved = resolveCapabilities(
1676
+ tool.name,
1677
+ tool.capabilities,
1678
+ { sessionId: request.sessionId, personalityId: request.personalityId },
1679
+ { ...this.backends, inboundAttachments: live?.inboundAttachments }
1680
+ );
1681
+ Object.assign(ctx, resolved);
1579
1682
  }
1683
+ return tool.execute(request.args, ctx);
1580
1684
  }
1581
- return errors;
1582
- }
1685
+ };
1583
1686
 
1584
1687
  // src/tool-registry.ts
1585
1688
  function needsBackends(caps) {
@@ -1620,13 +1723,53 @@ function safeReduce(r, result, ctx) {
1620
1723
  return result;
1621
1724
  }
1622
1725
  }
1726
+ var DEFAULT_CACHE_TTL_MS = 3e5;
1727
+ var MAX_CACHE_ENTRIES = 1e3;
1623
1728
  var DefaultToolRegistry = class {
1624
1729
  tools = /* @__PURE__ */ new Map();
1730
+ resultCache = /* @__PURE__ */ new Map();
1625
1731
  backends;
1626
1732
  reducers;
1627
- constructor(backends, reducers) {
1733
+ transport;
1734
+ // Per-turn live context — updated by executeParallel before dispatching.
1735
+ turnLiveCtx = { emit: () => {
1736
+ } };
1737
+ constructor(backends, reducers, transport) {
1628
1738
  this.backends = backends;
1629
1739
  this.reducers = reducers;
1740
+ this.transport = transport ?? new LocalToolTransport(
1741
+ (name) => this.tools.get(name)?.tool,
1742
+ backends,
1743
+ () => this.turnLiveCtx
1744
+ );
1745
+ }
1746
+ cacheGet(tool, args, ctx) {
1747
+ if (!tool.cache) return null;
1748
+ const opts = tool.cache === true ? {} : tool.cache;
1749
+ const keyPart = opts.keyFn ? opts.keyFn(args) : JSON.stringify(args);
1750
+ const key = `${tool.name}:${ctx.sessionId}:${ctx.personalityId ?? ""}:${keyPart}`;
1751
+ const entry = this.resultCache.get(key);
1752
+ if (!entry) return null;
1753
+ if (entry.expiresAt !== null && Date.now() > entry.expiresAt) {
1754
+ this.resultCache.delete(key);
1755
+ return null;
1756
+ }
1757
+ return entry.result;
1758
+ }
1759
+ cacheSet(tool, args, result, ctx) {
1760
+ if (!tool.cache) return;
1761
+ const opts = tool.cache === true ? {} : tool.cache;
1762
+ const keyPart = opts.keyFn ? opts.keyFn(args) : JSON.stringify(args);
1763
+ const key = `${tool.name}:${ctx.sessionId}:${ctx.personalityId ?? ""}:${keyPart}`;
1764
+ const ttl = opts.ttlMs ?? DEFAULT_CACHE_TTL_MS;
1765
+ const expiresAt = Date.now() + ttl;
1766
+ this.resultCache.set(key, { result, expiresAt });
1767
+ if (this.resultCache.size > MAX_CACHE_ENTRIES) {
1768
+ const oldest = this.resultCache.keys().next().value;
1769
+ if (oldest !== void 0) {
1770
+ this.resultCache.delete(oldest);
1771
+ }
1772
+ }
1630
1773
  }
1631
1774
  register(tool, opts) {
1632
1775
  this.tools.set(tool.name, { tool, pluginId: opts?.pluginId });
@@ -1665,6 +1808,10 @@ var DefaultToolRegistry = class {
1665
1808
  getForToolset(toolset) {
1666
1809
  return this.getAvailable().filter((t) => t.toolset === toolset);
1667
1810
  }
1811
+ /** v2.2 — Return the plugin id that registered a tool, if any. */
1812
+ getPluginId(name) {
1813
+ return this.tools.get(name)?.pluginId;
1814
+ }
1668
1815
  toDefinitions(allowedTools, filterOpts) {
1669
1816
  const entries = [...this.tools.values()].filter(
1670
1817
  (e) => !e.tool.isAvailable || e.tool.isAvailable()
@@ -1717,8 +1864,34 @@ var DefaultToolRegistry = class {
1717
1864
  // Runs all tool calls in parallel. Results are returned in input order.
1718
1865
  // Budget is split evenly across parallel calls; each result is post-trimmed to budget.
1719
1866
  // allowedTools + filterOpts enforce tool access at execution time (belt-and-suspenders).
1720
- async executeParallel(calls, ctx, allowedTools, filterOpts, turnAttachments) {
1867
+ async applyFilters(filters, tool, args, ctx, meta, execute) {
1868
+ const matching = filters.filter(
1869
+ (f) => (!f.toolName || (Array.isArray(f.toolName) ? f.toolName.includes(meta.toolName) : f.toolName === meta.toolName)) && (!f.toolset || tool.toolset === f.toolset)
1870
+ );
1871
+ let shortCircuit = null;
1872
+ for (const f of matching) {
1873
+ if (f.before) {
1874
+ const r = await f.before(args, ctx, meta);
1875
+ if (r !== null) {
1876
+ shortCircuit = r;
1877
+ break;
1878
+ }
1879
+ }
1880
+ }
1881
+ let result = shortCircuit ?? await execute();
1882
+ for (const f of matching) {
1883
+ if (f.after) result = await f.after(result, ctx, meta);
1884
+ }
1885
+ return result;
1886
+ }
1887
+ async executeParallel(calls, ctx, allowedTools, filterOpts, turnAttachments, filters) {
1721
1888
  const perCallBudget = Math.floor(ctx.resultBudgetChars / Math.max(calls.length, 1));
1889
+ this.turnLiveCtx = {
1890
+ emit: ctx.emit,
1891
+ readMtimes: ctx.readMtimes,
1892
+ storage: ctx.storage,
1893
+ inboundAttachments: turnAttachments
1894
+ };
1722
1895
  const results = await Promise.allSettled(
1723
1896
  calls.map(async (call) => {
1724
1897
  const entry = this.tools.get(call.name);
@@ -1767,6 +1940,10 @@ var DefaultToolRegistry = class {
1767
1940
  }
1768
1941
  };
1769
1942
  }
1943
+ const cached = this.cacheGet(entry.tool, call.args, ctx);
1944
+ if (cached) {
1945
+ return { toolCallId: call.toolCallId, name: call.name, result: cached };
1946
+ }
1770
1947
  if (needsBackends(entry.tool.capabilities) && !this.backends) {
1771
1948
  return {
1772
1949
  toolCallId: call.toolCallId,
@@ -1786,32 +1963,49 @@ var DefaultToolRegistry = class {
1786
1963
  result: synthesizeDryRunResult2(call.name, call.args)
1787
1964
  };
1788
1965
  }
1789
- const budget = Math.min(perCallBudget, entry.tool.maxResultChars ?? perCallBudget);
1790
- const toolCtx = { ...ctx, resultBudgetChars: budget };
1966
+ const cappedBudget = Math.min(perCallBudget, entry.tool.maxResultChars ?? perCallBudget);
1791
1967
  try {
1792
- if (needsBackends(entry.tool.capabilities) && this.backends) {
1793
- const resolved = resolveCapabilities(
1794
- entry.tool.name,
1795
- entry.tool.capabilities,
1796
- { sessionId: ctx.sessionId, personalityId: ctx.personalityId },
1797
- { ...this.backends, inboundAttachments: turnAttachments }
1798
- );
1799
- Object.assign(toolCtx, resolved);
1800
- }
1801
- const rawResult = await entry.tool.execute(call.args, toolCtx);
1968
+ const request = {
1969
+ toolCallId: call.toolCallId,
1970
+ name: call.name,
1971
+ args: call.args,
1972
+ sessionId: ctx.sessionId,
1973
+ sessionKey: ctx.sessionKey,
1974
+ platform: ctx.platform,
1975
+ workingDir: ctx.workingDir,
1976
+ personalityId: ctx.personalityId,
1977
+ teamId: ctx.teamId,
1978
+ agentId: ctx.agentId,
1979
+ memoryScopeId: ctx.memoryScopeId,
1980
+ userScopeId: ctx.userScopeId,
1981
+ currentTurn: ctx.currentTurn,
1982
+ messageCount: ctx.messageCount,
1983
+ resultBudgetChars: cappedBudget,
1984
+ networkPolicy: ctx.networkPolicy,
1985
+ dryRun: ctx.dryRun
1986
+ };
1987
+ const rawResult = filters && filters.length > 0 ? await this.applyFilters(
1988
+ filters,
1989
+ entry.tool,
1990
+ call.args,
1991
+ ctx,
1992
+ { toolName: call.name, toolCallId: call.toolCallId },
1993
+ () => this.transport.execute(request, ctx.abortSignal)
1994
+ ) : await this.transport.execute(request, ctx.abortSignal);
1802
1995
  const reducer = this.reducers?.get(call.name);
1803
1996
  const result = reducer ? safeReduce(reducer, rawResult, { args: call.args, turnCount: ctx.currentTurn ?? 0 }) : rawResult;
1804
- if (result.ok && result.value.length > budget) {
1997
+ if (result.ok && result.value.length > cappedBudget) {
1805
1998
  return {
1806
1999
  toolCallId: call.toolCallId,
1807
2000
  name: call.name,
1808
2001
  result: {
1809
2002
  ok: true,
1810
- value: `${result.value.slice(0, budget)}
2003
+ value: `${result.value.slice(0, cappedBudget)}
1811
2004
  [truncated \u2014 ${result.value.length} chars total]`
1812
2005
  }
1813
2006
  };
1814
2007
  }
2008
+ this.cacheSet(entry.tool, call.args, result, ctx);
1815
2009
  return { toolCallId: call.toolCallId, name: call.name, result };
1816
2010
  } catch (err) {
1817
2011
  return {
@@ -1854,7 +2048,12 @@ var KNOWN_AGENT_EVENT_TYPES = [
1854
2048
  "done",
1855
2049
  "context_meta",
1856
2050
  "run_start",
1857
- "dry_run_summary"
2051
+ "dry_run_summary",
2052
+ "tool_approval_required",
2053
+ "tool_approval_response",
2054
+ "evaluators_complete",
2055
+ "credential_required",
2056
+ "notification_received"
1858
2057
  ];
1859
2058
  function isKnownAgentEvent(event) {
1860
2059
  return KNOWN_AGENT_EVENT_TYPES.includes(event.type);
@@ -1896,10 +2095,16 @@ var AgentLoop = class {
1896
2095
  teamId;
1897
2096
  /** Per-personality MCP tool policy from mcp.yaml (NOT on PersonalityConfig). */
1898
2097
  mcpPolicy;
2098
+ /** v2.2 — Callback to emit per-tool invocation metrics to the diagnostic store. */
2099
+ onToolMetric;
2100
+ /** v2.2 — Pre-turn credential check callback. */
2101
+ credentialCheck;
1899
2102
  /** Per-session accumulated spend in USD. Keyed by sessionKey. Reset via resetSessionCost(). */
1900
2103
  sessionCosts = /* @__PURE__ */ new Map();
1901
2104
  /** FW-28 — per-session mtime registry. Keyed by sessionKey → (absPath → record). */
1902
2105
  sessionReadMtimes = /* @__PURE__ */ new Map();
2106
+ /** v2: per-run key/value store threaded into ToolContext for plugin communication. */
2107
+ contextStore = new ContextStore();
1903
2108
  constructor(config) {
1904
2109
  this.llm = config.llm;
1905
2110
  this.tools = config.tools ?? new DefaultToolRegistry();
@@ -1928,6 +2133,8 @@ var AgentLoop = class {
1928
2133
  if (config.clarifyBridge) this.clarifyBridge = config.clarifyBridge;
1929
2134
  if (config.requestDumpStore) this.requestDumpStore = config.requestDumpStore;
1930
2135
  if (config.mcpPolicy) this.mcpPolicy = config.mcpPolicy;
2136
+ if (config.onToolMetric) this.onToolMetric = config.onToolMetric;
2137
+ if (config.credentialCheck) this.credentialCheck = config.credentialCheck;
1931
2138
  this.contextEngines = config.contextEngines ?? new DefaultContextEngineRegistry();
1932
2139
  }
1933
2140
  /**
@@ -2061,6 +2268,26 @@ var AgentLoop = class {
2061
2268
  },
2062
2269
  allowedPlugins
2063
2270
  );
2271
+ if (this.credentialCheck) {
2272
+ const missing = await this.credentialCheck(sessionKey, text);
2273
+ if (missing) {
2274
+ if (traceId) this.observability?.endTrace(traceId, "error");
2275
+ this.observability?.flush();
2276
+ yield {
2277
+ type: "credential_required",
2278
+ pluginId: missing.pluginId,
2279
+ credentialKey: missing.credentialKey,
2280
+ kind: missing.kind,
2281
+ label: missing.label,
2282
+ description: missing.description,
2283
+ authUrl: missing.authUrl,
2284
+ sessionKey,
2285
+ pendingUserMessage: text
2286
+ };
2287
+ yield { type: "done", text: "", turnCount: 0 };
2288
+ return;
2289
+ }
2290
+ }
2064
2291
  const attachmentAnnotation = buildAttachmentAnnotation(opts.attachments ?? []);
2065
2292
  const annotatedText = attachmentAnnotation ? `${attachmentAnnotation}
2066
2293
  ${text}` : text;
@@ -2154,6 +2381,8 @@ ${text}` : text;
2154
2381
  if (promptCtx.meta && Object.keys(promptCtx.meta).length > 0) {
2155
2382
  yield { type: "context_meta", data: promptCtx.meta };
2156
2383
  }
2384
+ const rawSkills = promptCtx.meta?.skillFilesUsed;
2385
+ const activeSkillFiles = Array.isArray(rawSkills) && rawSkills.every((s) => typeof s === "string") ? rawSkills : void 0;
2157
2386
  if (memSnapshot && memSnapshot.entries.length > 0) {
2158
2387
  const blocks = [];
2159
2388
  const orderHints = {
@@ -2516,6 +2745,7 @@ ${rendered.slice(-MEMORY_MAX_CHARS)}`;
2516
2745
  sessionMtimes = /* @__PURE__ */ new Map();
2517
2746
  this.sessionReadMtimes.set(sessionKey, sessionMtimes);
2518
2747
  }
2748
+ this.contextStore.clear();
2519
2749
  const toolCtxBase = {
2520
2750
  sessionId,
2521
2751
  sessionKey,
@@ -2541,7 +2771,12 @@ ${rendered.slice(-MEMORY_MAX_CHARS)}`;
2541
2771
  resultBudgetChars: this.resultBudgetChars,
2542
2772
  readMtimes: sessionMtimes,
2543
2773
  ...scopedStorage ? { storage: scopedStorage } : {},
2544
- ...personality.safety?.network ? { networkPolicy: personality.safety.network } : {}
2774
+ ...personality.safety?.network ? { networkPolicy: personality.safety.network } : {},
2775
+ ...this.contextStore.asContextMethods(),
2776
+ llm: new SimpleCompletionImpl(this.llm, effectiveModel, ({ input, output }) => {
2777
+ llmInputTokens += input;
2778
+ llmOutputTokens += output;
2779
+ })
2545
2780
  };
2546
2781
  const prepped = [];
2547
2782
  const spanIds = /* @__PURE__ */ new Map();
@@ -2659,6 +2894,15 @@ ${rendered.slice(-MEMORY_MAX_CHARS)}`;
2659
2894
  obsConfig
2660
2895
  });
2661
2896
  spanIds.set(tc.toolCallId, spanId ?? "");
2897
+ const reqApprovalTool = this.tools.get(tc.toolName);
2898
+ if (reqApprovalTool?.requiresApproval) {
2899
+ yield {
2900
+ type: "tool_approval_required",
2901
+ toolCallId: tc.toolCallId,
2902
+ toolName: tc.toolName,
2903
+ args: effectiveArgs
2904
+ };
2905
+ }
2662
2906
  observe({ type: "tool_start", toolName: tc.toolName, args: effectiveArgs });
2663
2907
  yield {
2664
2908
  type: "tool_start",
@@ -2686,6 +2930,42 @@ ${rendered.slice(-MEMORY_MAX_CHARS)}`;
2686
2930
  opts.attachments
2687
2931
  ) : [];
2688
2932
  const execResultMap = new Map(execResults.map((r) => [r.toolCallId, r]));
2933
+ const directResult = execResults.find((r) => {
2934
+ const t = this.tools.get(r.name);
2935
+ return r.result.ok && t?.returnDirect;
2936
+ });
2937
+ if (directResult?.result.ok) {
2938
+ for (const p of prepped) {
2939
+ const execResult = execResultMap.get(p.toolCallId);
2940
+ const result = p.rejected ? { ok: false, error: p.rejected, code: "execution_failed" } : execResult?.result ?? {
2941
+ ok: false,
2942
+ error: "Tool result missing",
2943
+ code: "execution_failed"
2944
+ };
2945
+ await this.session.appendMessage({
2946
+ sessionId,
2947
+ role: "tool_result",
2948
+ content: result.ok ? result.value : result.error,
2949
+ toolCallId: p.toolCallId,
2950
+ toolName: p.name
2951
+ });
2952
+ }
2953
+ for (const r of execResults) {
2954
+ yield {
2955
+ type: "tool_end",
2956
+ toolCallId: r.toolCallId,
2957
+ toolName: r.name,
2958
+ ok: r.result.ok,
2959
+ durationMs: Date.now() - startedAt,
2960
+ result: r.result.ok ? r.result.value : r.result.error
2961
+ };
2962
+ }
2963
+ fullText = directResult.result.value;
2964
+ if (traceId) this.observability?.endTrace(traceId, "ok");
2965
+ this.observability?.flush();
2966
+ yield { type: "done", text: fullText, turnCount };
2967
+ return;
2968
+ }
2689
2969
  if (opts.dryRun) {
2690
2970
  for (const input of execInputs) {
2691
2971
  dryRunPlan.push({
@@ -2768,6 +3048,19 @@ ${rendered.slice(-MEMORY_MAX_CHARS)}`;
2768
3048
  },
2769
3049
  allowedPlugins
2770
3050
  );
3051
+ if (this.onToolMetric) {
3052
+ const pluginId = this.tools.getPluginId?.(p.name);
3053
+ if (pluginId) {
3054
+ this.onToolMetric({
3055
+ pluginId,
3056
+ toolName: p.name,
3057
+ ok: result.ok,
3058
+ durationMs,
3059
+ sessionId,
3060
+ turnId: String(turnCount)
3061
+ });
3062
+ }
3063
+ }
2771
3064
  const touchedPath = extractFilePath(p.args);
2772
3065
  if (touchedPath !== void 0) {
2773
3066
  await this.hooks.fireVoid(
@@ -2853,7 +3146,8 @@ ${rendered.slice(-MEMORY_MAX_CHARS)}`;
2853
3146
  successfulToolCalls,
2854
3147
  totalToolCalls,
2855
3148
  toolNames: [...toolNameCounts.keys()],
2856
- initialPrompt: text
3149
+ initialPrompt: text,
3150
+ activeSkillFiles
2857
3151
  },
2858
3152
  allowedPlugins
2859
3153
  );
@@ -3658,6 +3952,27 @@ var LastWriteWinsPolicy = class {
3658
3952
  }
3659
3953
  };
3660
3954
 
3955
+ // src/notification-router.ts
3956
+ var DefaultNotificationRouter = class {
3957
+ adapters = /* @__PURE__ */ new Map();
3958
+ async route(pluginId, opts) {
3959
+ if (opts.sessionKey === "*") return;
3960
+ const adapter = this.adapters.get(opts.sessionKey);
3961
+ if (!adapter) return;
3962
+ if (opts.startTurn) {
3963
+ await adapter.injectUserMessage(opts.message);
3964
+ } else {
3965
+ await adapter.send(opts.message, opts.payload);
3966
+ }
3967
+ }
3968
+ register(sessionKey, adapter) {
3969
+ this.adapters.set(sessionKey, adapter);
3970
+ }
3971
+ deregister(sessionKey) {
3972
+ this.adapters.delete(sessionKey);
3973
+ }
3974
+ };
3975
+
3661
3976
  // src/path-boundary.ts
3662
3977
  import { resolve as resolve5, sep as sep2 } from "path";
3663
3978
  function assertWithinBase(base, target) {
@@ -4056,10 +4371,12 @@ export {
4056
4371
  ClarifyBusyError,
4057
4372
  ClarifyNoSurfaceError,
4058
4373
  ClarifyTimedOutNoDefaultError,
4374
+ ContextStore,
4059
4375
  DefaultContextEngineRegistry,
4060
4376
  DefaultHookRegistry,
4061
4377
  DefaultLLMProviderRegistry,
4062
4378
  DefaultMemoryProviderRegistry,
4379
+ DefaultNotificationRouter,
4063
4380
  DefaultPersonalityRegistry,
4064
4381
  DefaultToolRegistry,
4065
4382
  DefaultToolResultReducerRegistry,
@@ -4071,6 +4388,7 @@ export {
4071
4388
  KNOWN_AGENT_EVENT_TYPES,
4072
4389
  LastWriteWinsPolicy,
4073
4390
  LazyOnDemandPolicy,
4391
+ LocalToolTransport,
4074
4392
  MemoryConflictError2 as MemoryConflictError,
4075
4393
  NoopMemoryProvider,
4076
4394
  PluginRegistry,
@@ -4080,6 +4398,7 @@ export {
4080
4398
  ScopedProcessImpl,
4081
4399
  ScopedSecretsImpl,
4082
4400
  SemanticSummaryEngine,
4401
+ SimpleCompletionImpl,
4083
4402
  SsrfError,
4084
4403
  applyTemporalDecay,
4085
4404
  assertWithinBase,