agent-world 0.13.0 → 0.15.0

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 (263) hide show
  1. package/README.md +90 -17
  2. package/dist/cli/commands.d.ts +7 -1
  3. package/dist/cli/commands.js +27 -10
  4. package/dist/cli/hitl.d.ts +4 -1
  5. package/dist/cli/hitl.js +55 -20
  6. package/dist/cli/index.js +249 -97
  7. package/dist/cli/system-events.d.ts +27 -0
  8. package/dist/cli/system-events.js +63 -0
  9. package/dist/core/activity-tracker.d.ts +26 -0
  10. package/dist/core/activity-tracker.d.ts.map +1 -1
  11. package/dist/core/activity-tracker.js +21 -4
  12. package/dist/core/activity-tracker.js.map +1 -1
  13. package/dist/core/anthropic-direct.d.ts +2 -0
  14. package/dist/core/anthropic-direct.d.ts.map +1 -1
  15. package/dist/core/anthropic-direct.js +43 -1
  16. package/dist/core/anthropic-direct.js.map +1 -1
  17. package/dist/core/chat-constants.d.ts +12 -0
  18. package/dist/core/chat-constants.d.ts.map +1 -1
  19. package/dist/core/chat-constants.js +5 -0
  20. package/dist/core/chat-constants.js.map +1 -1
  21. package/dist/core/create-agent-tool.d.ts +5 -0
  22. package/dist/core/create-agent-tool.d.ts.map +1 -1
  23. package/dist/core/create-agent-tool.js +57 -34
  24. package/dist/core/create-agent-tool.js.map +1 -1
  25. package/dist/core/events/index.d.ts +5 -2
  26. package/dist/core/events/index.d.ts.map +1 -1
  27. package/dist/core/events/index.js +5 -2
  28. package/dist/core/events/index.js.map +1 -1
  29. package/dist/core/events/memory-manager.d.ts +26 -1
  30. package/dist/core/events/memory-manager.d.ts.map +1 -1
  31. package/dist/core/events/memory-manager.js +877 -72
  32. package/dist/core/events/memory-manager.js.map +1 -1
  33. package/dist/core/events/orchestrator.d.ts +8 -0
  34. package/dist/core/events/orchestrator.d.ts.map +1 -1
  35. package/dist/core/events/orchestrator.js +203 -36
  36. package/dist/core/events/orchestrator.js.map +1 -1
  37. package/dist/core/events/persistence.d.ts +21 -14
  38. package/dist/core/events/persistence.d.ts.map +1 -1
  39. package/dist/core/events/persistence.js +100 -35
  40. package/dist/core/events/persistence.js.map +1 -1
  41. package/dist/core/events/publishers.d.ts +13 -7
  42. package/dist/core/events/publishers.d.ts.map +1 -1
  43. package/dist/core/events/publishers.js +53 -37
  44. package/dist/core/events/publishers.js.map +1 -1
  45. package/dist/core/events/subscribers.d.ts +17 -14
  46. package/dist/core/events/subscribers.d.ts.map +1 -1
  47. package/dist/core/events/subscribers.js +61 -148
  48. package/dist/core/events/subscribers.js.map +1 -1
  49. package/dist/core/events/title-scheduler.d.ts +27 -0
  50. package/dist/core/events/title-scheduler.d.ts.map +1 -0
  51. package/dist/core/events/title-scheduler.js +135 -0
  52. package/dist/core/events/title-scheduler.js.map +1 -0
  53. package/dist/core/events/tool-bridge-logging.d.ts +4 -1
  54. package/dist/core/events/tool-bridge-logging.d.ts.map +1 -1
  55. package/dist/core/events/tool-bridge-logging.js +112 -13
  56. package/dist/core/events/tool-bridge-logging.js.map +1 -1
  57. package/dist/core/events-metadata.d.ts.map +1 -1
  58. package/dist/core/events-metadata.js +8 -4
  59. package/dist/core/events-metadata.js.map +1 -1
  60. package/dist/core/export.d.ts +1 -1
  61. package/dist/core/export.d.ts.map +1 -1
  62. package/dist/core/export.js +2 -15
  63. package/dist/core/export.js.map +1 -1
  64. package/dist/core/feature-path-logging.d.ts +50 -0
  65. package/dist/core/feature-path-logging.d.ts.map +1 -0
  66. package/dist/core/feature-path-logging.js +130 -0
  67. package/dist/core/feature-path-logging.js.map +1 -0
  68. package/dist/core/file-tools.d.ts +57 -1
  69. package/dist/core/file-tools.d.ts.map +1 -1
  70. package/dist/core/file-tools.js +329 -29
  71. package/dist/core/file-tools.js.map +1 -1
  72. package/dist/core/google-direct.d.ts +6 -1
  73. package/dist/core/google-direct.d.ts.map +1 -1
  74. package/dist/core/google-direct.js +76 -7
  75. package/dist/core/google-direct.js.map +1 -1
  76. package/dist/core/heartbeat.d.ts +34 -0
  77. package/dist/core/heartbeat.d.ts.map +1 -0
  78. package/dist/core/heartbeat.js +153 -0
  79. package/dist/core/heartbeat.js.map +1 -0
  80. package/dist/core/hitl-tool.d.ts +6 -12
  81. package/dist/core/hitl-tool.d.ts.map +1 -1
  82. package/dist/core/hitl-tool.js +66 -88
  83. package/dist/core/hitl-tool.js.map +1 -1
  84. package/dist/core/hitl.d.ts +61 -4
  85. package/dist/core/hitl.d.ts.map +1 -1
  86. package/dist/core/hitl.js +324 -60
  87. package/dist/core/hitl.js.map +1 -1
  88. package/dist/core/index.d.ts +11 -7
  89. package/dist/core/index.d.ts.map +1 -1
  90. package/dist/core/index.js +10 -6
  91. package/dist/core/index.js.map +1 -1
  92. package/dist/core/llm-manager.d.ts +15 -0
  93. package/dist/core/llm-manager.d.ts.map +1 -1
  94. package/dist/core/llm-manager.js +325 -40
  95. package/dist/core/llm-manager.js.map +1 -1
  96. package/dist/core/load-skill-tool.d.ts +36 -3
  97. package/dist/core/load-skill-tool.d.ts.map +1 -1
  98. package/dist/core/load-skill-tool.js +807 -93
  99. package/dist/core/load-skill-tool.js.map +1 -1
  100. package/dist/core/logger.d.ts +14 -0
  101. package/dist/core/logger.d.ts.map +1 -1
  102. package/dist/core/logger.js +15 -0
  103. package/dist/core/logger.js.map +1 -1
  104. package/dist/core/managers.d.ts +18 -50
  105. package/dist/core/managers.d.ts.map +1 -1
  106. package/dist/core/managers.js +340 -502
  107. package/dist/core/managers.js.map +1 -1
  108. package/dist/core/mcp-server-registry.d.ts +16 -1
  109. package/dist/core/mcp-server-registry.d.ts.map +1 -1
  110. package/dist/core/mcp-server-registry.js +162 -12
  111. package/dist/core/mcp-server-registry.js.map +1 -1
  112. package/dist/core/message-cutoff.d.ts +29 -0
  113. package/dist/core/message-cutoff.d.ts.map +1 -0
  114. package/dist/core/message-cutoff.js +63 -0
  115. package/dist/core/message-cutoff.js.map +1 -0
  116. package/dist/core/message-edit-manager.d.ts +54 -0
  117. package/dist/core/message-edit-manager.d.ts.map +1 -0
  118. package/dist/core/message-edit-manager.js +602 -0
  119. package/dist/core/message-edit-manager.js.map +1 -0
  120. package/dist/core/message-prep.d.ts +2 -0
  121. package/dist/core/message-prep.d.ts.map +1 -1
  122. package/dist/core/message-prep.js +39 -12
  123. package/dist/core/message-prep.js.map +1 -1
  124. package/dist/core/message-processing-control.d.ts +1 -0
  125. package/dist/core/message-processing-control.d.ts.map +1 -1
  126. package/dist/core/message-processing-control.js +23 -6
  127. package/dist/core/message-processing-control.js.map +1 -1
  128. package/dist/core/openai-direct.d.ts +9 -3
  129. package/dist/core/openai-direct.d.ts.map +1 -1
  130. package/dist/core/openai-direct.js +267 -33
  131. package/dist/core/openai-direct.js.map +1 -1
  132. package/dist/core/optional-tracers/opik-runtime.d.ts +32 -0
  133. package/dist/core/optional-tracers/opik-runtime.d.ts.map +1 -0
  134. package/dist/core/optional-tracers/opik-runtime.js +141 -0
  135. package/dist/core/optional-tracers/opik-runtime.js.map +1 -0
  136. package/dist/core/queue-manager.d.ts +84 -0
  137. package/dist/core/queue-manager.d.ts.map +1 -0
  138. package/dist/core/queue-manager.js +814 -0
  139. package/dist/core/queue-manager.js.map +1 -0
  140. package/dist/core/reasoning-controls.d.ts +30 -0
  141. package/dist/core/reasoning-controls.d.ts.map +1 -0
  142. package/dist/core/reasoning-controls.js +118 -0
  143. package/dist/core/reasoning-controls.js.map +1 -0
  144. package/dist/core/reliability-config.d.ts +82 -0
  145. package/dist/core/reliability-config.d.ts.map +1 -0
  146. package/dist/core/reliability-config.js +106 -0
  147. package/dist/core/reliability-config.js.map +1 -0
  148. package/dist/core/reliability-runtime.d.ts +53 -0
  149. package/dist/core/reliability-runtime.d.ts.map +1 -0
  150. package/dist/core/reliability-runtime.js +92 -0
  151. package/dist/core/reliability-runtime.js.map +1 -0
  152. package/dist/core/security/guardrails.d.ts +21 -0
  153. package/dist/core/security/guardrails.d.ts.map +1 -0
  154. package/dist/core/security/guardrails.js +111 -0
  155. package/dist/core/security/guardrails.js.map +1 -0
  156. package/dist/core/send-message-tool.d.ts +79 -0
  157. package/dist/core/send-message-tool.d.ts.map +1 -0
  158. package/dist/core/send-message-tool.js +222 -0
  159. package/dist/core/send-message-tool.js.map +1 -0
  160. package/dist/core/shell-cmd-tool.d.ts +82 -1
  161. package/dist/core/shell-cmd-tool.d.ts.map +1 -1
  162. package/dist/core/shell-cmd-tool.js +854 -42
  163. package/dist/core/shell-cmd-tool.js.map +1 -1
  164. package/dist/core/skill-registry.d.ts +2 -0
  165. package/dist/core/skill-registry.d.ts.map +1 -1
  166. package/dist/core/skill-registry.js +52 -2
  167. package/dist/core/skill-registry.js.map +1 -1
  168. package/dist/core/storage/eventStorage/fileEventStorage.d.ts +5 -0
  169. package/dist/core/storage/eventStorage/fileEventStorage.d.ts.map +1 -1
  170. package/dist/core/storage/eventStorage/fileEventStorage.js +61 -0
  171. package/dist/core/storage/eventStorage/fileEventStorage.js.map +1 -1
  172. package/dist/core/storage/eventStorage/memoryEventStorage.d.ts +5 -0
  173. package/dist/core/storage/eventStorage/memoryEventStorage.d.ts.map +1 -1
  174. package/dist/core/storage/eventStorage/memoryEventStorage.js +34 -0
  175. package/dist/core/storage/eventStorage/memoryEventStorage.js.map +1 -1
  176. package/dist/core/storage/eventStorage/sqliteEventStorage.d.ts +1 -0
  177. package/dist/core/storage/eventStorage/sqliteEventStorage.d.ts.map +1 -1
  178. package/dist/core/storage/eventStorage/sqliteEventStorage.js +19 -2
  179. package/dist/core/storage/eventStorage/sqliteEventStorage.js.map +1 -1
  180. package/dist/core/storage/eventStorage/types.d.ts +6 -0
  181. package/dist/core/storage/eventStorage/types.d.ts.map +1 -1
  182. package/dist/core/storage/eventStorage/types.js +1 -0
  183. package/dist/core/storage/eventStorage/types.js.map +1 -1
  184. package/dist/core/storage/eventStorage/validation.d.ts.map +1 -1
  185. package/dist/core/storage/eventStorage/validation.js +2 -1
  186. package/dist/core/storage/eventStorage/validation.js.map +1 -1
  187. package/dist/core/storage/github-world-import.d.ts +84 -0
  188. package/dist/core/storage/github-world-import.d.ts.map +1 -0
  189. package/dist/core/storage/github-world-import.js +365 -0
  190. package/dist/core/storage/github-world-import.js.map +1 -0
  191. package/dist/core/storage/memory-storage.d.ts +19 -8
  192. package/dist/core/storage/memory-storage.d.ts.map +1 -1
  193. package/dist/core/storage/memory-storage.js +147 -49
  194. package/dist/core/storage/memory-storage.js.map +1 -1
  195. package/dist/core/storage/queue-storage.d.ts +1 -0
  196. package/dist/core/storage/queue-storage.d.ts.map +1 -1
  197. package/dist/core/storage/queue-storage.js +3 -2
  198. package/dist/core/storage/queue-storage.js.map +1 -1
  199. package/dist/core/storage/sqlite-storage.d.ts +14 -9
  200. package/dist/core/storage/sqlite-storage.d.ts.map +1 -1
  201. package/dist/core/storage/sqlite-storage.js +131 -154
  202. package/dist/core/storage/sqlite-storage.js.map +1 -1
  203. package/dist/core/storage/storage-factory.d.ts +3 -0
  204. package/dist/core/storage/storage-factory.d.ts.map +1 -1
  205. package/dist/core/storage/storage-factory.js +175 -89
  206. package/dist/core/storage/storage-factory.js.map +1 -1
  207. package/dist/core/storage/world-storage.d.ts +1 -1
  208. package/dist/core/storage/world-storage.d.ts.map +1 -1
  209. package/dist/core/storage/world-storage.js +5 -1
  210. package/dist/core/storage/world-storage.js.map +1 -1
  211. package/dist/core/storage-init.d.ts +11 -0
  212. package/dist/core/storage-init.d.ts.map +1 -0
  213. package/dist/core/storage-init.js +122 -0
  214. package/dist/core/storage-init.js.map +1 -0
  215. package/dist/core/subscription.d.ts +8 -1
  216. package/dist/core/subscription.d.ts.map +1 -1
  217. package/dist/core/subscription.js +130 -23
  218. package/dist/core/subscription.js.map +1 -1
  219. package/dist/core/tool-approval.d.ts +45 -0
  220. package/dist/core/tool-approval.d.ts.map +1 -0
  221. package/dist/core/tool-approval.js +223 -0
  222. package/dist/core/tool-approval.js.map +1 -0
  223. package/dist/core/tool-execution-envelope.d.ts +87 -0
  224. package/dist/core/tool-execution-envelope.d.ts.map +1 -0
  225. package/dist/core/tool-execution-envelope.js +168 -0
  226. package/dist/core/tool-execution-envelope.js.map +1 -0
  227. package/dist/core/tool-utils.d.ts +7 -2
  228. package/dist/core/tool-utils.d.ts.map +1 -1
  229. package/dist/core/tool-utils.js +81 -17
  230. package/dist/core/tool-utils.js.map +1 -1
  231. package/dist/core/types.d.ts +67 -19
  232. package/dist/core/types.d.ts.map +1 -1
  233. package/dist/core/types.js +3 -0
  234. package/dist/core/types.js.map +1 -1
  235. package/dist/core/utils.d.ts +7 -0
  236. package/dist/core/utils.d.ts.map +1 -1
  237. package/dist/core/utils.js +71 -21
  238. package/dist/core/utils.js.map +1 -1
  239. package/dist/core/web-fetch-tool.d.ts +72 -0
  240. package/dist/core/web-fetch-tool.d.ts.map +1 -0
  241. package/dist/core/web-fetch-tool.js +491 -0
  242. package/dist/core/web-fetch-tool.js.map +1 -0
  243. package/dist/core/world-registry.d.ts +84 -0
  244. package/dist/core/world-registry.d.ts.map +1 -0
  245. package/dist/core/world-registry.js +247 -0
  246. package/dist/core/world-registry.js.map +1 -0
  247. package/dist/public/assets/index-Be-1xtV-.js +104 -0
  248. package/dist/public/assets/index-tsDdiXDU.css +1 -0
  249. package/dist/public/index.html +2 -2
  250. package/dist/public/mcp-sandbox-proxy.html +148 -0
  251. package/dist/server/api.js +260 -18
  252. package/dist/server/error-response.d.ts +27 -0
  253. package/dist/server/error-response.js +77 -0
  254. package/dist/server/index.d.ts +2 -1
  255. package/dist/server/index.js +6 -2
  256. package/dist/server/sse-handler.d.ts +11 -1
  257. package/dist/server/sse-handler.js +194 -34
  258. package/migrations/0015_add_message_queue.sql +36 -0
  259. package/migrations/0016_add_world_heartbeat.sql +13 -0
  260. package/migrations/0017_add_title_provenance.sql +7 -0
  261. package/package.json +31 -10
  262. package/dist/public/assets/index-BW41BxMy.css +0 -1
  263. package/dist/public/assets/index-kO6UJFwK.js +0 -96
@@ -18,9 +18,19 @@
18
18
  * - Keeps payload format deterministic for stable downstream parsing
19
19
  *
20
20
  * Recent Changes:
21
- * - 2026-02-16: Treat non-zero exit status from instruction-referenced skill scripts as execution errors so UI receives tool-error feedback.
22
- * - 2026-02-15: Reject `load_skill` script execution when a referenced script resolves outside the active execution working directory.
23
- * - 2026-02-15: Execute skill scripts in the project working directory (from context) instead of the skill root.
21
+ * - 2026-03-12: Added `read` permission level guard to script execution instructions load normally but all script execution steps are blocked with an inline note when toolPermission is 'read'.
22
+ * - 2026-03-06: Removed `world.currentChatId` fallback from interactive approval/result scoping; interactive `load_skill` now requires explicit `context.chatId`.
23
+ * - 2026-03-01: Removed minimal-check mode branch so `load_skill` always runs script/reference preflight consistently and keeps script-root execution guidance available.
24
+ * - 2026-03-01: Relaxed execution-directive narration requirements to avoid mandatory pre-tool plan text and reduce token overhead.
25
+ * - 2026-03-01: Added skill-description thread-through and acknowledgment-first execution directive requirements after successful `load_skill`, including unconditional pre-execution plan narration.
26
+ * - 2026-02-28: Updated `<execution_directive>` to require concise tool-use intent text before tool calls and to allow mixed text + tool-call turns when supported by the provider.
27
+ * - 2026-02-27: Run-scoped `load_skill` cache now stores only successful outcomes so declined/error/not-found paths remain retryable in the same run.
28
+ * - 2026-02-27: Added run-scoped `load_skill` result caching keyed by latest user-turn marker so repeated same-skill calls are auto-suppressed across assistant/tool hops.
29
+ * - 2026-02-27: Replaced `[load_skill:hitl]` console logs with structured category logger events (`load_skill.hitl`).
30
+ * - 2026-02-27: Added same-turn `yes_once` approval reuse + in-flight approval dedupe so repeated/concurrent `load_skill` calls for the same skill do not spam duplicate HITL prompts.
31
+ * - 2026-02-25: Added persisted synthetic HITL tool-call/response messages for `load_skill` approval so frontends can reconstruct prompts from memory without relying on transient event timing.
32
+ * - 2026-02-24: Surface non-zero script exits as informational output instead of blocking errors (scripts may be CLI tools requiring args).
33
+ * - 2026-02-24: Execute skill scripts with cwd set to trusted working directory when provided, otherwise use the skill root.
24
34
  * - 2026-02-14: Strip SKILL.md YAML front matter from injected `<instructions>` content.
25
35
  * - 2026-02-14: Omit `<active_resources>` from `load_skill` payloads when no instruction-referenced scripts are present.
26
36
  * - 2026-02-14: Added skill-level HITL gating so `load_skill` requires approval even when no script references are present.
@@ -29,14 +39,25 @@
29
39
  import { promises as fs } from 'fs';
30
40
  import * as path from 'path';
31
41
  import { getSkill, getSkillSourcePath, getSkillSourceScope, waitForInitialSkillSync, } from './skill-registry.js';
42
+ import { createStorageWithWrappers } from './storage/storage-factory.js';
32
43
  import { executeShellCommand, formatResultForLLM, validateShellCommandScope, } from './shell-cmd-tool.js';
44
+ import { buildToolArtifactPreviewUrl, createArtifactToolPreview, createTextToolPreview, createUrlToolPreview, guessMediaTypeFromPath, parseToolExecutionEnvelopeContent, serializeToolExecutionEnvelope, } from './tool-execution-envelope.js';
45
+ import { createCategoryLogger } from './logger.js';
33
46
  import { parseSkillIdListFromEnv } from './skill-settings.js';
34
- import { requestWorldOption } from './hitl.js';
47
+ import { generateId, getEnvValueFromText } from './utils.js';
48
+ import { requestToolApproval } from './tool-approval.js';
35
49
  const APPROVAL_OPTION_YES_ONCE = 'yes_once';
36
50
  const APPROVAL_OPTION_YES_IN_SESSION = 'yes_in_session';
37
51
  const APPROVAL_OPTION_NO = 'no';
38
52
  const SCRIPT_TIMEOUT_MS = 120_000;
53
+ const LOAD_SKILL_RUN_RESULT_CACHE_LIMIT = 256;
39
54
  const skillSessionApprovals = new Set();
55
+ const skillTurnApprovals = new Set();
56
+ const inFlightSkillApprovals = new Map();
57
+ const loadSkillRunResultCache = new Map();
58
+ const inFlightLoadSkillRunResults = new Map();
59
+ const loggerLoadSkillHitl = createCategoryLogger('load_skill.hitl');
60
+ const LOAD_SKILL_PREVIEW_SCRIPT_OUTPUT_CHARS = 800;
40
61
  class SkillScriptExecutionError extends Error {
41
62
  constructor(message) {
42
63
  super(message);
@@ -92,6 +113,13 @@ function buildDisabledBySettingsResult(skillId) {
92
113
  '</skill_context>',
93
114
  ].join('\n');
94
115
  }
116
+ function truncatePreviewText(text, maxChars = LOAD_SKILL_PREVIEW_SCRIPT_OUTPUT_CHARS) {
117
+ const normalized = String(text || '').trim();
118
+ if (normalized.length <= maxChars) {
119
+ return normalized;
120
+ }
121
+ return `${normalized.slice(0, maxChars)}\n...[truncated ${normalized.length - maxChars} chars]`;
122
+ }
95
123
  function isSkillEnabledBySettings(skillId) {
96
124
  const includeGlobalSkills = String(process.env.AGENT_WORLD_ENABLE_GLOBAL_SKILLS ?? 'true').toLowerCase() !== 'false';
97
125
  const includeProjectSkills = String(process.env.AGENT_WORLD_ENABLE_PROJECT_SKILLS ?? 'true').toLowerCase() !== 'false';
@@ -110,14 +138,33 @@ function isSkillEnabledBySettings(skillId) {
110
138
  function normalizeScriptPath(scriptPath) {
111
139
  return scriptPath.replace(/\\/g, '/').replace(/^\.\//, '').trim();
112
140
  }
141
+ function normalizeComparablePath(targetPath) {
142
+ const resolvedPath = path.resolve(targetPath).replace(/\\/g, '/');
143
+ return process.platform === 'win32'
144
+ ? resolvedPath.replace(/\/+/g, '/').replace(/\/$/, '')
145
+ : resolvedPath.replace(/\/+/g, '/').replace(/\/$/, '') || '/';
146
+ }
147
+ function normalizeAbsoluteLocalPath(targetPath) {
148
+ return normalizeComparablePath(targetPath);
149
+ }
150
+ function toRootRelativePath(rootPath, targetPath) {
151
+ const normalizedRoot = normalizeComparablePath(rootPath);
152
+ const normalizedTarget = normalizeComparablePath(targetPath);
153
+ if (normalizedTarget === normalizedRoot) {
154
+ return '';
155
+ }
156
+ if (normalizedTarget.startsWith(`${normalizedRoot}/`)) {
157
+ return normalizedTarget.slice(normalizedRoot.length + 1);
158
+ }
159
+ return normalizedTarget.replace(/^\/+/, '');
160
+ }
113
161
  function stripYamlFrontMatter(markdown) {
114
162
  const frontMatterPattern = /^\uFEFF?---\s*\r?\n[\s\S]*?\r?\n---\s*(?:\r?\n|$)/;
115
163
  return markdown.replace(frontMatterPattern, '');
116
164
  }
117
165
  function isPathWithinRoot(skillRoot, targetPath) {
118
- const normalize = (value) => path.resolve(value).replace(/\\/g, '/').replace(/\/+/g, '/').replace(/\/$/, '');
119
- const normalizedRoot = normalize(skillRoot);
120
- const normalizedTarget = normalize(targetPath);
166
+ const normalizedRoot = normalizeComparablePath(skillRoot);
167
+ const normalizedTarget = normalizeComparablePath(targetPath);
121
168
  return normalizedTarget === normalizedRoot || normalizedTarget.startsWith(`${normalizedRoot}/`);
122
169
  }
123
170
  function extractReferencedScriptPaths(markdown) {
@@ -160,14 +207,14 @@ async function collectReferenceFiles(skillRoot, markdown) {
160
207
  if (linkedPath.startsWith('#')) {
161
208
  continue;
162
209
  }
163
- const absolutePath = path.resolve(skillRoot, linkedPath);
210
+ const absolutePath = normalizeAbsoluteLocalPath(path.resolve(skillRoot, linkedPath));
164
211
  if (!isPathWithinRoot(skillRoot, absolutePath)) {
165
212
  continue;
166
213
  }
167
214
  try {
168
215
  const stat = await fs.stat(absolutePath);
169
216
  if (stat.isFile()) {
170
- collected.add(path.relative(skillRoot, absolutePath).replace(/\\/g, '/'));
217
+ collected.add(toRootRelativePath(skillRoot, absolutePath));
171
218
  }
172
219
  }
173
220
  catch {
@@ -197,12 +244,12 @@ async function collectReferenceFiles(skillRoot, markdown) {
197
244
  }
198
245
  entries.sort((left, right) => left.name.localeCompare(right.name));
199
246
  for (const entry of entries) {
200
- const absolutePath = path.join(currentPath, entry.name);
247
+ const absolutePath = normalizeAbsoluteLocalPath(path.join(currentPath, entry.name));
201
248
  if (entry.isDirectory()) {
202
249
  queue.push(absolutePath);
203
250
  }
204
251
  else if (entry.isFile()) {
205
- collected.add(path.relative(skillRoot, absolutePath).replace(/\\/g, '/'));
252
+ collected.add(toRootRelativePath(skillRoot, absolutePath));
206
253
  }
207
254
  }
208
255
  }
@@ -230,54 +277,442 @@ function formatReferenceFilesBlock(referenceFiles) {
230
277
  function createSessionApprovalKey(worldId, chatId, skillId) {
231
278
  return `${worldId}::${chatId ?? 'global'}::${skillId}`;
232
279
  }
280
+ function getExplicitContextChatId(context) {
281
+ const chatId = typeof context?.chatId === 'string' ? context.chatId.trim() : '';
282
+ return chatId || null;
283
+ }
284
+ function getCurrentTurnMarker(context) {
285
+ const chatId = getExplicitContextChatId(context);
286
+ const messages = Array.isArray(context?.messages) ? context.messages : [];
287
+ if (!chatId || messages.length === 0) {
288
+ return null;
289
+ }
290
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
291
+ const message = messages[index];
292
+ if (message?.role !== 'user') {
293
+ continue;
294
+ }
295
+ const messageChatId = message?.chatId ? String(message.chatId).trim() : null;
296
+ if (messageChatId && messageChatId !== chatId) {
297
+ continue;
298
+ }
299
+ const messageId = String(message?.messageId || '').trim();
300
+ if (messageId) {
301
+ return `msg:${messageId}`;
302
+ }
303
+ const createdAt = message?.createdAt ? new Date(message.createdAt) : null;
304
+ if (createdAt && Number.isFinite(createdAt.valueOf())) {
305
+ return `ts:${createdAt.toISOString()}`;
306
+ }
307
+ const content = String(message?.content || '').trim();
308
+ if (content) {
309
+ return `content:${content.slice(0, 80)}`;
310
+ }
311
+ return `idx:${index}`;
312
+ }
313
+ return null;
314
+ }
315
+ function createTurnApprovalKey(worldId, chatId, skillId, turnMarker) {
316
+ return `${worldId}::${chatId ?? 'global'}::${skillId}::turn::${turnMarker}`;
317
+ }
318
+ function createRunResultKey(worldId, chatId, skillId, turnMarker) {
319
+ return `${worldId}::${chatId ?? 'global'}::${skillId}::run::${turnMarker}`;
320
+ }
321
+ /**
322
+ * Reconstruct skill approval caches from persisted message history.
323
+ * Called during chat restore so that `yes_in_session` and `yes_once` grants
324
+ * survive app restarts without re-prompting the user.
325
+ *
326
+ * Scans `role: 'tool'` messages whose JSON content contains `skillId` and
327
+ * `optionId` fields (written by `persistLoadSkillApprovalResolutionMessage`).
328
+ *
329
+ * - `yes_in_session` grants are restored unconditionally (session-scoped).
330
+ * - `yes_once` grants are restored only when they belong to the current turn
331
+ * (i.e. appear after the last `role: 'user'` message in the history).
332
+ */
333
+ export function reconstructSkillApprovalsFromMessages(worldId, chatId, messages) {
334
+ if (!worldId || !Array.isArray(messages) || messages.length === 0) {
335
+ return 0;
336
+ }
337
+ // Find the index of the last user message for this chat (turn boundary).
338
+ let lastUserMessageIndex = -1;
339
+ let turnMarker = null;
340
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
341
+ const msg = messages[i];
342
+ if (msg?.role !== 'user')
343
+ continue;
344
+ const msgChatId = msg?.chatId ? String(msg.chatId).trim() : null;
345
+ if (msgChatId && msgChatId !== (chatId ?? 'global'))
346
+ continue;
347
+ lastUserMessageIndex = i;
348
+ // Derive turn marker using same precedence as getCurrentTurnMarker.
349
+ const messageId = String(msg?.messageId || '').trim();
350
+ if (messageId) {
351
+ turnMarker = `msg:${messageId}`;
352
+ }
353
+ else {
354
+ const createdAt = msg?.createdAt ? new Date(msg.createdAt) : null;
355
+ if (createdAt && Number.isFinite(createdAt.valueOf())) {
356
+ turnMarker = `ts:${createdAt.toISOString()}`;
357
+ }
358
+ else {
359
+ const content = String(msg?.content || '').trim();
360
+ if (content) {
361
+ turnMarker = `content:${content.slice(0, 80)}`;
362
+ }
363
+ else {
364
+ turnMarker = `idx:${i}`;
365
+ }
366
+ }
367
+ }
368
+ break;
369
+ }
370
+ let restored = 0;
371
+ for (let i = 0; i < messages.length; i += 1) {
372
+ const msg = messages[i];
373
+ if (msg?.role !== 'tool')
374
+ continue;
375
+ let payload = null;
376
+ try {
377
+ const content = typeof msg.content === 'string' ? msg.content : null;
378
+ if (content)
379
+ payload = JSON.parse(content);
380
+ }
381
+ catch {
382
+ continue;
383
+ }
384
+ if (!payload
385
+ || typeof payload.requestId !== 'string'
386
+ || !payload.requestId.includes('load_skill_approval')
387
+ || typeof payload.skillId !== 'string'
388
+ || typeof payload.optionId !== 'string') {
389
+ continue;
390
+ }
391
+ const { skillId, optionId } = payload;
392
+ if (optionId === APPROVAL_OPTION_YES_IN_SESSION) {
393
+ skillSessionApprovals.add(createSessionApprovalKey(worldId, chatId, skillId));
394
+ restored += 1;
395
+ }
396
+ else if (optionId === APPROVAL_OPTION_YES_ONCE && turnMarker && i > lastUserMessageIndex) {
397
+ skillTurnApprovals.add(createTurnApprovalKey(worldId, chatId, skillId, turnMarker));
398
+ restored += 1;
399
+ }
400
+ }
401
+ if (restored > 0) {
402
+ loggerLoadSkillHitl.debug('Reconstructed skill approvals from message history', {
403
+ worldId,
404
+ chatId: chatId || null,
405
+ restored,
406
+ });
407
+ }
408
+ return restored;
409
+ }
410
+ /**
411
+ * Clear cached skill approvals and run results scoped to a specific chat.
412
+ * Must be called when messages are removed (e.g. edit+resubmit) so that
413
+ * HITL approval prompts fire again for the reprocessed message.
414
+ */
415
+ export function clearChatSkillApprovals(worldId, chatId) {
416
+ const chatToken = chatId ?? 'global';
417
+ const prefix = `${worldId}::${chatToken}::`;
418
+ for (const key of skillSessionApprovals) {
419
+ if (key.startsWith(prefix)) {
420
+ skillSessionApprovals.delete(key);
421
+ }
422
+ }
423
+ for (const key of skillTurnApprovals) {
424
+ if (key.startsWith(prefix)) {
425
+ skillTurnApprovals.delete(key);
426
+ }
427
+ }
428
+ for (const key of inFlightSkillApprovals.keys()) {
429
+ if (key.includes(prefix)) {
430
+ inFlightSkillApprovals.delete(key);
431
+ }
432
+ }
433
+ for (const key of loadSkillRunResultCache.keys()) {
434
+ if (key.startsWith(prefix)) {
435
+ loadSkillRunResultCache.delete(key);
436
+ }
437
+ }
438
+ for (const key of inFlightLoadSkillRunResults.keys()) {
439
+ if (key.startsWith(prefix)) {
440
+ inFlightLoadSkillRunResults.delete(key);
441
+ }
442
+ }
443
+ }
444
+ function getRunScopedLoadSkillResultKey(skillId, context) {
445
+ const worldId = String(context?.world?.id || '').trim();
446
+ if (!worldId) {
447
+ return null;
448
+ }
449
+ const chatId = getExplicitContextChatId(context);
450
+ if (!chatId) {
451
+ return null;
452
+ }
453
+ const turnMarker = getCurrentTurnMarker(context);
454
+ if (!turnMarker) {
455
+ return null;
456
+ }
457
+ return createRunResultKey(worldId, chatId, skillId, turnMarker);
458
+ }
459
+ function rememberRunScopedLoadSkillResult(cacheKey, result) {
460
+ if (loadSkillRunResultCache.has(cacheKey)) {
461
+ loadSkillRunResultCache.delete(cacheKey);
462
+ }
463
+ loadSkillRunResultCache.set(cacheKey, result);
464
+ while (loadSkillRunResultCache.size > LOAD_SKILL_RUN_RESULT_CACHE_LIMIT) {
465
+ const oldestKey = loadSkillRunResultCache.keys().next().value;
466
+ if (!oldestKey) {
467
+ break;
468
+ }
469
+ loadSkillRunResultCache.delete(oldestKey);
470
+ }
471
+ }
472
+ function getLoadSkillApprovalRequestId(context, skillId) {
473
+ const parentToolCallId = String(context?.toolCallId || '').trim();
474
+ if (parentToolCallId) {
475
+ return `${parentToolCallId}::load_skill_approval`;
476
+ }
477
+ const normalizedSkillId = String(skillId || '').trim() || 'skill';
478
+ return `load_skill_approval::${normalizedSkillId}::${generateId()}`;
479
+ }
480
+ async function persistAgentMemoryIfAvailable(context) {
481
+ const worldId = String(context?.world?.id || '').trim();
482
+ const agentName = String(context?.agentName || '').trim();
483
+ const messages = Array.isArray(context?.messages) ? context.messages : null;
484
+ const world = context?.world;
485
+ if (!worldId || !agentName || !messages || !world?.agents || typeof world.agents.get !== 'function') {
486
+ return;
487
+ }
488
+ const agent = world.agents.get(agentName);
489
+ if (!agent) {
490
+ return;
491
+ }
492
+ const storage = await createStorageWithWrappers();
493
+ await storage.saveAgent(worldId, agent);
494
+ }
495
+ async function persistLoadSkillApprovalPromptMessage(options) {
496
+ const messages = Array.isArray(options.context?.messages) ? options.context.messages : null;
497
+ if (!messages) {
498
+ return;
499
+ }
500
+ const existing = messages.some((message) => message?.role === 'assistant'
501
+ && Array.isArray(message?.tool_calls)
502
+ && message.tool_calls.some((toolCall) => String(toolCall?.id || '').trim() === options.requestId));
503
+ if (existing) {
504
+ return;
505
+ }
506
+ const question = `Skill "${options.skillId}" requested execution.${options.scriptPaths.length > 0 ? ` Referenced scripts:\n${options.scriptPaths.map((scriptPath) => `- ${scriptPath}`).join('\n')}` : ''}\n\nApprove applying this skill now?`;
507
+ const toolArguments = {
508
+ question,
509
+ options: [
510
+ { id: APPROVAL_OPTION_YES_ONCE, label: 'Yes once' },
511
+ { id: APPROVAL_OPTION_YES_IN_SESSION, label: 'Yes in this session' },
512
+ { id: APPROVAL_OPTION_NO, label: 'No' },
513
+ ],
514
+ defaultOptionId: APPROVAL_OPTION_NO,
515
+ defaultOption: 'No',
516
+ metadata: {
517
+ tool: 'human_intervention_request',
518
+ toolCallId: options.requestId,
519
+ source: 'load_skill',
520
+ skillId: options.skillId,
521
+ scriptPaths: options.scriptPaths,
522
+ },
523
+ };
524
+ const chatId = getExplicitContextChatId(options.context);
525
+ if (!chatId) {
526
+ return;
527
+ }
528
+ const agentName = String(options.context?.agentName || '').trim() || 'assistant';
529
+ messages.push({
530
+ role: 'assistant',
531
+ content: `Calling tool: human_intervention_request (skill_id: "${options.skillId}")`,
532
+ tool_calls: [{
533
+ id: options.requestId,
534
+ type: 'function',
535
+ function: {
536
+ name: 'human_intervention_request',
537
+ arguments: JSON.stringify(toolArguments),
538
+ },
539
+ }],
540
+ sender: agentName,
541
+ createdAt: new Date(),
542
+ chatId,
543
+ messageId: generateId(),
544
+ replyToMessageId: options.context?.toolCallId,
545
+ agentId: agentName,
546
+ });
547
+ await persistAgentMemoryIfAvailable(options.context);
548
+ loggerLoadSkillHitl.debug('Persisted load_skill approval tool-call message', {
549
+ chatId: chatId || null,
550
+ agentName,
551
+ requestId: options.requestId,
552
+ skillId: options.skillId,
553
+ });
554
+ }
555
+ async function persistLoadSkillApprovalResolutionMessage(options) {
556
+ const messages = Array.isArray(options.context?.messages) ? options.context.messages : null;
557
+ if (!messages) {
558
+ return;
559
+ }
560
+ const existing = messages.some((message) => message?.role === 'tool'
561
+ && String(message?.tool_call_id || '').trim() === options.requestId);
562
+ if (existing) {
563
+ return;
564
+ }
565
+ const chatId = getExplicitContextChatId(options.context);
566
+ if (!chatId) {
567
+ return;
568
+ }
569
+ const agentName = String(options.context?.agentName || '').trim() || 'assistant';
570
+ const payload = {
571
+ requestId: options.resolution.requestId,
572
+ optionId: options.resolution.optionId,
573
+ source: options.resolution.source,
574
+ skillId: options.skillId,
575
+ };
576
+ messages.push({
577
+ role: 'tool',
578
+ content: JSON.stringify(payload),
579
+ tool_call_id: options.requestId,
580
+ sender: agentName,
581
+ createdAt: new Date(),
582
+ chatId,
583
+ messageId: generateId(),
584
+ agentId: agentName,
585
+ });
586
+ await persistAgentMemoryIfAvailable(options.context);
587
+ loggerLoadSkillHitl.debug('Persisted load_skill approval tool-result message', {
588
+ chatId: chatId || null,
589
+ agentName,
590
+ requestId: options.requestId,
591
+ optionId: options.resolution.optionId,
592
+ });
593
+ }
233
594
  async function requestSkillExecutionApproval(options) {
234
- const worldId = String(options.context?.world?.id || '').trim();
235
- const chatId = options.context?.chatId ?? options.context?.world?.currentChatId ?? null;
236
- if (!worldId || !options.context?.world) {
595
+ const worldContext = options.context?.world;
596
+ const worldId = String(worldContext?.id || '').trim();
597
+ const chatId = getExplicitContextChatId(options.context);
598
+ const requestId = getLoadSkillApprovalRequestId(options.context, options.skillId);
599
+ if (!worldId || !worldContext) {
237
600
  return true;
238
601
  }
602
+ if (!chatId) {
603
+ return false;
604
+ }
239
605
  const sessionApprovalKey = createSessionApprovalKey(worldId, chatId, options.skillId);
606
+ const turnMarker = getCurrentTurnMarker(options.context);
607
+ const turnApprovalKey = turnMarker
608
+ ? createTurnApprovalKey(worldId, chatId, options.skillId, turnMarker)
609
+ : null;
240
610
  if (skillSessionApprovals.has(sessionApprovalKey)) {
241
611
  return true;
242
612
  }
243
- const scriptSummary = options.scriptPaths.length > 0
244
- ? `The skill references local scripts:\n${options.scriptPaths.map((scriptPath) => `- ${scriptPath}`).join('\n')}`
245
- : 'No instruction-referenced local scripts were detected for this skill.';
246
- const approval = await requestWorldOption(options.context.world, {
247
- title: `Run skill ${options.skillId}?`,
248
- message: [
249
- `Skill "${options.skillId}" requested execution.`,
250
- scriptSummary,
251
- 'Approve applying this skill now?',
252
- ].join('\n\n'),
253
- chatId,
254
- defaultOptionId: APPROVAL_OPTION_NO,
255
- options: [
256
- { id: APPROVAL_OPTION_YES_ONCE, label: 'Yes once', description: 'Allow this skill for this call only.' },
257
- {
258
- id: APPROVAL_OPTION_YES_IN_SESSION,
259
- label: 'Yes in this session',
260
- description: 'Allow this skill for the current chat session.',
613
+ if (turnApprovalKey && skillTurnApprovals.has(turnApprovalKey)) {
614
+ return true;
615
+ }
616
+ const inFlightApprovalKey = turnApprovalKey
617
+ ? `turn::${turnApprovalKey}`
618
+ : `session::${sessionApprovalKey}`;
619
+ const existingInFlightApproval = inFlightSkillApprovals.get(inFlightApprovalKey);
620
+ if (existingInFlightApproval) {
621
+ return await existingInFlightApproval;
622
+ }
623
+ const approvalPromise = (async () => {
624
+ const scriptSummary = options.scriptPaths.length > 0
625
+ ? `The skill references local scripts:\n${options.scriptPaths.map((scriptPath) => `- ${scriptPath}`).join('\n')}`
626
+ : 'No instruction-referenced local scripts were detected for this skill.';
627
+ await persistLoadSkillApprovalPromptMessage({
628
+ context: options.context,
629
+ requestId,
630
+ skillId: options.skillId,
631
+ scriptPaths: options.scriptPaths,
632
+ });
633
+ const approvalResult = await requestToolApproval({
634
+ world: worldContext,
635
+ requestId,
636
+ title: `Run skill ${options.skillId}?`,
637
+ message: [
638
+ `Skill "${options.skillId}" requested execution.`,
639
+ scriptSummary,
640
+ 'Approve applying this skill now?',
641
+ ].join('\n\n'),
642
+ chatId,
643
+ defaultOptionId: APPROVAL_OPTION_NO,
644
+ options: [
645
+ { id: APPROVAL_OPTION_YES_ONCE, label: 'Yes once', description: 'Allow this skill for this call only.' },
646
+ {
647
+ id: APPROVAL_OPTION_YES_IN_SESSION,
648
+ label: 'Yes in this session',
649
+ description: 'Allow this skill for the current chat session.',
650
+ },
651
+ { id: APPROVAL_OPTION_NO, label: 'No', description: 'Do not apply this skill now.' },
652
+ ],
653
+ approvedOptionIds: [APPROVAL_OPTION_YES_ONCE, APPROVAL_OPTION_YES_IN_SESSION],
654
+ metadata: {
655
+ tool: 'human_intervention_request',
656
+ toolCallId: requestId,
657
+ source: 'load_skill',
658
+ skillId: options.skillId,
659
+ scriptPaths: options.scriptPaths,
261
660
  },
262
- { id: APPROVAL_OPTION_NO, label: 'No', description: 'Do not apply this skill now.' },
263
- ],
264
- metadata: { skillId: options.skillId, scriptPaths: options.scriptPaths },
265
- });
266
- if (approval.optionId === APPROVAL_OPTION_NO) {
267
- return false;
661
+ agentName: options.context?.agentName ?? null,
662
+ });
663
+ const approval = {
664
+ requestId,
665
+ worldId,
666
+ chatId,
667
+ optionId: approvalResult.optionId,
668
+ source: approvalResult.source,
669
+ };
670
+ await persistLoadSkillApprovalResolutionMessage({
671
+ context: options.context,
672
+ requestId,
673
+ resolution: approval,
674
+ skillId: options.skillId,
675
+ });
676
+ if (!approvalResult.approved) {
677
+ return false;
678
+ }
679
+ if (approval.optionId === APPROVAL_OPTION_YES_IN_SESSION) {
680
+ skillSessionApprovals.add(sessionApprovalKey);
681
+ if (turnApprovalKey) {
682
+ skillTurnApprovals.add(turnApprovalKey);
683
+ }
684
+ }
685
+ else if (approval.optionId === APPROVAL_OPTION_YES_ONCE && turnApprovalKey) {
686
+ skillTurnApprovals.add(turnApprovalKey);
687
+ }
688
+ return true;
689
+ })();
690
+ inFlightSkillApprovals.set(inFlightApprovalKey, approvalPromise);
691
+ try {
692
+ return await approvalPromise;
268
693
  }
269
- if (approval.optionId === APPROVAL_OPTION_YES_IN_SESSION) {
270
- skillSessionApprovals.add(sessionApprovalKey);
694
+ finally {
695
+ const currentInFlightApproval = inFlightSkillApprovals.get(inFlightApprovalKey);
696
+ if (currentInFlightApproval === approvalPromise) {
697
+ inFlightSkillApprovals.delete(inFlightApprovalKey);
698
+ }
271
699
  }
272
- return true;
273
700
  }
274
701
  async function executeSkillScripts(options) {
275
702
  const scriptPaths = options.scriptPaths;
276
703
  if (scriptPaths.length === 0) {
277
704
  return [];
278
705
  }
706
+ // Check world-level tool permission: 'read' blocks all script execution steps.
707
+ const toolPermission = getEnvValueFromText(options.context?.world?.variables, 'tool_permission') ?? 'auto';
708
+ if (toolPermission === 'read') {
709
+ return scriptPaths.map((scriptPath) => ({
710
+ source: scriptPath,
711
+ output: 'Script execution is blocked by the current permission level (read).',
712
+ }));
713
+ }
279
714
  const worldId = String(options.context?.world?.id || '').trim();
280
- const chatId = options.context?.chatId ?? options.context?.world?.currentChatId ?? null;
715
+ const chatId = getExplicitContextChatId(options.context);
281
716
  const executionDirectory = options.context?.workingDirectory || options.skillRoot;
282
717
  if (!worldId || !options.context?.world) {
283
718
  return [{
@@ -329,13 +764,6 @@ async function executeSkillScripts(options) {
329
764
  continue;
330
765
  }
331
766
  const absoluteCommandSpec = resolveScriptCommand(normalizedAbsoluteScriptPath);
332
- if (!isPathWithinRoot(executionDirectory, normalizedAbsoluteScriptPath)) {
333
- scriptOutputs.push({
334
- source: relativeScriptPath,
335
- output: `Script path rejected: "${relativeScriptPath}" resolves outside execution working directory "${executionDirectory}".`,
336
- });
337
- continue;
338
- }
339
767
  const executionResult = await executeShellCommand(absoluteCommandSpec.command, absoluteCommandSpec.parameters, executionDirectory, {
340
768
  timeout: SCRIPT_TIMEOUT_MS,
341
769
  abortSignal: options.context?.abortSignal,
@@ -346,8 +774,17 @@ async function executeSkillScripts(options) {
346
774
  if (executionResult.exitCode !== 0 || executionResult.error) {
347
775
  const exitCode = executionResult.exitCode === null ? 'unknown' : String(executionResult.exitCode);
348
776
  const stderr = executionResult.stderr.trim();
349
- const stderrSuffix = stderr ? ` stderr: ${stderr}` : '';
350
- throw new SkillScriptExecutionError(`Skill script "${relativeScriptPath}" failed with exit code ${exitCode}.${stderrSuffix}`);
777
+ const stdoutPreview = executionResult.stdout.trim();
778
+ const detail = [
779
+ `exit code ${exitCode}`,
780
+ stderr ? `stderr: ${stderr}` : '',
781
+ stdoutPreview ? `stdout: ${stdoutPreview}` : '',
782
+ ].filter(Boolean).join(' | ');
783
+ scriptOutputs.push({
784
+ source: relativeScriptPath,
785
+ output: `Script exited with ${detail}`,
786
+ });
787
+ continue;
351
788
  }
352
789
  scriptOutputs.push({
353
790
  source: relativeScriptPath,
@@ -356,10 +793,161 @@ async function executeSkillScripts(options) {
356
793
  }
357
794
  return scriptOutputs;
358
795
  }
796
+ function isYouTubeUrl(value) {
797
+ return /https?:\/\/(?:www\.)?(?:youtube\.com|youtu\.be)\//i.test(String(value || ''));
798
+ }
799
+ function extractUrlsFromText(text) {
800
+ const matches = String(text || '').match(/https?:\/\/[^\s)>\]}]+/gi) || [];
801
+ return [...new Set(matches.map((match) => match.replace(/[),.;!?]+$/g, '')))];
802
+ }
803
+ function extractArtifactPathCandidates(text) {
804
+ const matches = String(text || '').match(/(?:\/[^\s"'`<>]+|\.[/\\][^\s"'`<>]+|[A-Za-z0-9_.-]+\.(?:svg|png|jpg|jpeg|gif|webp|mp3|wav|ogg|m4a|mp4|webm|mov|md|txt))/gi) || [];
805
+ return [...new Set(matches)];
806
+ }
807
+ async function resolvePreviewArtifactFromCandidate(candidate, roots, options = {}) {
808
+ const normalizedCandidate = String(candidate || '').trim();
809
+ if (!normalizedCandidate) {
810
+ return null;
811
+ }
812
+ for (const root of roots) {
813
+ const absolutePath = path.isAbsolute(normalizedCandidate)
814
+ ? normalizeAbsoluteLocalPath(normalizedCandidate)
815
+ : normalizeAbsoluteLocalPath(path.resolve(root, normalizedCandidate));
816
+ const isAllowedPath = roots.some((allowedRoot) => isPathWithinRoot(allowedRoot, absolutePath));
817
+ if (!isAllowedPath) {
818
+ continue;
819
+ }
820
+ try {
821
+ const stat = await fs.stat(absolutePath);
822
+ if (!stat.isFile()) {
823
+ continue;
824
+ }
825
+ const mediaType = guessMediaTypeFromPath(absolutePath);
826
+ return createArtifactToolPreview({
827
+ path: absolutePath,
828
+ bytes: stat.size,
829
+ media_type: mediaType,
830
+ display_name: path.basename(absolutePath),
831
+ url: buildToolArtifactPreviewUrl({ path: absolutePath, worldId: options.worldId }),
832
+ });
833
+ }
834
+ catch {
835
+ continue;
836
+ }
837
+ }
838
+ return null;
839
+ }
840
+ async function collectLoadSkillPreviewArtifacts(options) {
841
+ const previews = [];
842
+ const seen = new Set();
843
+ const roots = [
844
+ options.skillRoot,
845
+ ...(options.context?.workingDirectory ? [options.context.workingDirectory] : []),
846
+ ];
847
+ for (const referenceFile of options.referenceFiles) {
848
+ const absolutePath = path.isAbsolute(referenceFile)
849
+ ? normalizeAbsoluteLocalPath(referenceFile)
850
+ : normalizeAbsoluteLocalPath(path.resolve(options.skillRoot, referenceFile));
851
+ try {
852
+ const stat = await fs.stat(absolutePath);
853
+ if (!stat.isFile()) {
854
+ continue;
855
+ }
856
+ const key = `path:${absolutePath}`;
857
+ if (seen.has(key)) {
858
+ continue;
859
+ }
860
+ seen.add(key);
861
+ previews.push(createArtifactToolPreview({
862
+ path: absolutePath,
863
+ bytes: stat.size,
864
+ media_type: guessMediaTypeFromPath(absolutePath),
865
+ display_name: path.basename(referenceFile),
866
+ title: referenceFile,
867
+ url: buildToolArtifactPreviewUrl({
868
+ path: absolutePath,
869
+ worldId: typeof options.context?.world?.id === 'string' ? options.context.world.id : undefined,
870
+ }),
871
+ }));
872
+ }
873
+ catch {
874
+ continue;
875
+ }
876
+ }
877
+ for (const output of options.scriptOutputs) {
878
+ for (const url of extractUrlsFromText(output.output)) {
879
+ const key = `url:${url}`;
880
+ if (seen.has(key)) {
881
+ continue;
882
+ }
883
+ seen.add(key);
884
+ previews.push(createUrlToolPreview(url, {
885
+ renderer: isYouTubeUrl(url) ? 'youtube' : undefined,
886
+ text: output.source,
887
+ title: output.source,
888
+ }));
889
+ }
890
+ for (const candidate of extractArtifactPathCandidates(output.output)) {
891
+ const preview = await resolvePreviewArtifactFromCandidate(candidate, roots, {
892
+ worldId: typeof options.context?.world?.id === 'string' ? options.context.world.id : undefined,
893
+ });
894
+ if (!preview) {
895
+ continue;
896
+ }
897
+ if (!('artifact' in preview)) {
898
+ continue;
899
+ }
900
+ const key = `artifact:${JSON.stringify(preview.artifact)}`;
901
+ if (seen.has(key)) {
902
+ continue;
903
+ }
904
+ seen.add(key);
905
+ previews.push(preview);
906
+ }
907
+ }
908
+ return previews;
909
+ }
910
+ async function buildLoadSkillSuccessPreview(options) {
911
+ const summaryLines = [
912
+ `Loaded skill \`${options.skillId}\`.`,
913
+ '',
914
+ `${options.skillDescription}`,
915
+ '',
916
+ `Referenced scripts: ${options.scriptPaths.length > 0 ? options.scriptPaths.join(', ') : '(none)'}`,
917
+ `Reference files: ${options.referenceFiles.length > 0 ? options.referenceFiles.join(', ') : '(none)'}`,
918
+ ];
919
+ if (options.scriptOutputs.length > 0) {
920
+ summaryLines.push('');
921
+ summaryLines.push('Script outputs:');
922
+ for (const scriptOutput of options.scriptOutputs) {
923
+ summaryLines.push(`- ${scriptOutput.source}: ${truncatePreviewText(scriptOutput.output)}`);
924
+ }
925
+ }
926
+ const previews = [
927
+ createTextToolPreview(summaryLines.join('\n'), {
928
+ markdown: true,
929
+ title: `load_skill ${options.skillId}`,
930
+ }),
931
+ ];
932
+ previews.push(...await collectLoadSkillPreviewArtifacts(options));
933
+ return previews;
934
+ }
935
+ function wrapLoadSkillToolResult(options) {
936
+ return serializeToolExecutionEnvelope({
937
+ __type: 'tool_execution_envelope',
938
+ version: 1,
939
+ tool: 'load_skill',
940
+ ...(options.toolCallId ? { tool_call_id: options.toolCallId } : {}),
941
+ status: /<error>/i.test(options.result) ? 'failed' : 'completed',
942
+ preview: options.preview,
943
+ result: options.result,
944
+ });
945
+ }
359
946
  function buildSuccessResult(options) {
360
- const { skillId, skillName, markdown, scriptOutputs, referenceFiles } = options;
947
+ const { skillId, skillName, skillDescription, skillRoot, markdown, scriptOutputs, referenceFiles, scriptPaths, } = options;
361
948
  const escapedSkillId = escapeXmlText(skillId);
362
949
  const escapedSkillName = escapeXmlText(skillName);
950
+ const escapedSkillDescription = escapeXmlText(skillDescription);
363
951
  const hasActiveResources = scriptOutputs.length > 0;
364
952
  const scriptBlocks = scriptOutputs.flatMap((scriptOutput) => ([
365
953
  ` <script_output source="${escapeXmlText(scriptOutput.source)}">`,
@@ -379,14 +967,21 @@ function buildSuccessResult(options) {
379
967
  '',
380
968
  ]
381
969
  : [];
970
+ const hasReferencedScripts = scriptPaths.length > 0;
382
971
  const executionDirective = [
383
972
  ' <execution_directive>',
384
973
  ` You are now operating under the specialized ${escapedSkillName} protocol.`,
385
- ' 1. Prioritize the logic in <instructions> over generic behavior.',
974
+ ` Skill purpose: ${escapedSkillDescription}`,
975
+ ' 1. Acknowledge which skill was loaded and apply it directly to the user request.',
976
+ ' 2. Prioritize the logic in <instructions> over generic behavior.',
386
977
  hasActiveResources
387
- ? ' 2. Use the data in <active_resources> to complete the user\'s specific request.'
388
- : ' 2. Use the skill instructions to complete the user\'s specific request.',
389
- ' 3. If the workflow is multi-step, explicitly state your plan before executing.',
978
+ ? ' 3. Use the data in <active_resources> to complete the user\'s specific request.'
979
+ : ' 3. Use the skill instructions to complete the user\'s specific request.',
980
+ ' 4. Execute required steps directly; avoid unnecessary planning narration unless the user explicitly asks for a plan.',
981
+ ' 5. Keep tool-related assistant text concise and result-focused.',
982
+ ...(hasReferencedScripts
983
+ ? [` 6. Scripts referenced in <instructions> are located at skill root: ${escapeXmlText(skillRoot)}. When invoking them via shell commands, construct the absolute path (e.g., ${escapeXmlText(skillRoot)}/scripts/example.py) since they may not be accessible via relative paths from the project directory.`]
984
+ : []),
390
985
  ' </execution_directive>',
391
986
  ];
392
987
  return [
@@ -402,7 +997,7 @@ function buildSuccessResult(options) {
402
997
  }
403
998
  export function createLoadSkillToolDefinition() {
404
999
  return {
405
- description: 'Load full SKILL.md instructions by skill_id from the skill registry. Use this when a request matches a listed skill.',
1000
+ description: 'Load full SKILL.md instructions by skill_id from the skill registry. Use this when a request matches a listed skill. After loading, apply the skill instructions directly to the user request.',
406
1001
  parameters: {
407
1002
  type: 'object',
408
1003
  properties: {
@@ -416,51 +1011,170 @@ export function createLoadSkillToolDefinition() {
416
1011
  },
417
1012
  execute: async (args, _sequenceId, _parentToolCall, context) => {
418
1013
  await waitForInitialSkillSync();
1014
+ const persistToolEnvelope = context?.persistToolEnvelope === true;
1015
+ const toolCallId = typeof context?.toolCallId === 'string' ? context.toolCallId : undefined;
419
1016
  const requestedSkillId = typeof args?.skill_id === 'string' ? args.skill_id.trim() : '';
420
1017
  if (!requestedSkillId) {
421
- return buildReadErrorResult('', 'Missing required parameter: skill_id');
1018
+ const result = buildReadErrorResult('', 'Missing required parameter: skill_id');
1019
+ return persistToolEnvelope
1020
+ ? wrapLoadSkillToolResult({
1021
+ result,
1022
+ preview: createTextToolPreview('Missing required parameter: skill_id'),
1023
+ toolCallId,
1024
+ })
1025
+ : result;
422
1026
  }
423
- const entry = getSkill(requestedSkillId);
424
- const sourcePath = getSkillSourcePath(requestedSkillId);
425
- if (!entry || !sourcePath) {
426
- return buildNotFoundResult(requestedSkillId);
1027
+ const runScopedResultKey = getRunScopedLoadSkillResultKey(requestedSkillId, context);
1028
+ if (runScopedResultKey) {
1029
+ const cachedResult = loadSkillRunResultCache.get(runScopedResultKey);
1030
+ if (cachedResult !== undefined) {
1031
+ loggerLoadSkillHitl.debug('Returning cached run-scoped load_skill result', {
1032
+ skillId: requestedSkillId,
1033
+ runScopedResultKey,
1034
+ });
1035
+ return cachedResult;
1036
+ }
1037
+ const inFlightResult = inFlightLoadSkillRunResults.get(runScopedResultKey);
1038
+ if (inFlightResult) {
1039
+ return await inFlightResult;
1040
+ }
427
1041
  }
428
- if (!isSkillEnabledBySettings(requestedSkillId)) {
429
- return buildDisabledBySettingsResult(requestedSkillId);
1042
+ const computeResult = async () => {
1043
+ const entry = getSkill(requestedSkillId);
1044
+ const sourcePath = getSkillSourcePath(requestedSkillId);
1045
+ if (!entry || !sourcePath) {
1046
+ const result = buildNotFoundResult(requestedSkillId);
1047
+ return {
1048
+ result,
1049
+ cacheableForRun: false,
1050
+ };
1051
+ }
1052
+ if (!isSkillEnabledBySettings(requestedSkillId)) {
1053
+ const result = buildDisabledBySettingsResult(requestedSkillId);
1054
+ return {
1055
+ result,
1056
+ cacheableForRun: false,
1057
+ };
1058
+ }
1059
+ try {
1060
+ const markdown = await fs.readFile(sourcePath, 'utf8');
1061
+ const instructionsMarkdown = stripYamlFrontMatter(markdown);
1062
+ const skillRoot = path.dirname(sourcePath);
1063
+ const scriptPaths = extractReferencedScriptPaths(instructionsMarkdown);
1064
+ const toolPermission = getEnvValueFromText(context?.world?.variables, 'tool_permission') ?? 'auto';
1065
+ if (context?.world && !getExplicitContextChatId(context)) {
1066
+ const result = buildReadErrorResult(requestedSkillId, 'Interactive load_skill execution requires an explicit chatId.');
1067
+ return {
1068
+ result,
1069
+ cacheableForRun: false,
1070
+ };
1071
+ }
1072
+ if (toolPermission === 'ask') {
1073
+ const isApproved = await requestSkillExecutionApproval({
1074
+ skillId: requestedSkillId,
1075
+ scriptPaths,
1076
+ context,
1077
+ });
1078
+ if (!isApproved) {
1079
+ const result = buildDeclinedResult(requestedSkillId);
1080
+ return {
1081
+ result,
1082
+ cacheableForRun: false,
1083
+ };
1084
+ }
1085
+ }
1086
+ const scriptOutputs = await executeSkillScripts({
1087
+ scriptPaths,
1088
+ skillRoot,
1089
+ context,
1090
+ });
1091
+ const referenceFiles = await collectReferenceFiles(skillRoot, instructionsMarkdown);
1092
+ const result = buildSuccessResult({
1093
+ skillId: requestedSkillId,
1094
+ skillName: entry.skill_id,
1095
+ skillDescription: entry.description?.trim() || entry.skill_id,
1096
+ skillRoot,
1097
+ markdown: instructionsMarkdown,
1098
+ scriptOutputs,
1099
+ referenceFiles,
1100
+ scriptPaths,
1101
+ });
1102
+ if (!persistToolEnvelope) {
1103
+ return {
1104
+ result,
1105
+ cacheableForRun: true,
1106
+ };
1107
+ }
1108
+ const preview = await buildLoadSkillSuccessPreview({
1109
+ skillId: requestedSkillId,
1110
+ skillDescription: entry.description?.trim() || entry.skill_id,
1111
+ skillRoot,
1112
+ scriptOutputs,
1113
+ referenceFiles,
1114
+ scriptPaths,
1115
+ context,
1116
+ });
1117
+ return {
1118
+ result: wrapLoadSkillToolResult({
1119
+ result,
1120
+ preview,
1121
+ toolCallId,
1122
+ }),
1123
+ cacheableForRun: true,
1124
+ };
1125
+ }
1126
+ catch (error) {
1127
+ if (error instanceof SkillScriptExecutionError) {
1128
+ throw error;
1129
+ }
1130
+ const message = error instanceof Error ? error.message : String(error);
1131
+ const result = buildReadErrorResult(requestedSkillId, message);
1132
+ return {
1133
+ result: persistToolEnvelope
1134
+ ? wrapLoadSkillToolResult({
1135
+ result,
1136
+ preview: createTextToolPreview(message),
1137
+ toolCallId,
1138
+ })
1139
+ : result,
1140
+ cacheableForRun: false,
1141
+ };
1142
+ }
1143
+ };
1144
+ if (!runScopedResultKey) {
1145
+ const outcome = await computeResult();
1146
+ if (persistToolEnvelope && !parseToolExecutionEnvelopeContent(outcome.result)) {
1147
+ return wrapLoadSkillToolResult({
1148
+ result: outcome.result,
1149
+ preview: createTextToolPreview(truncatePreviewText(outcome.result, 1600)),
1150
+ toolCallId,
1151
+ });
1152
+ }
1153
+ return outcome.result;
430
1154
  }
1155
+ const runScopedPromise = computeResult().then((outcome) => {
1156
+ if (outcome.cacheableForRun) {
1157
+ rememberRunScopedLoadSkillResult(runScopedResultKey, outcome.result);
1158
+ }
1159
+ return outcome.result;
1160
+ });
1161
+ inFlightLoadSkillRunResults.set(runScopedResultKey, runScopedPromise);
431
1162
  try {
432
- const markdown = await fs.readFile(sourcePath, 'utf8');
433
- const instructionsMarkdown = stripYamlFrontMatter(markdown);
434
- const skillRoot = path.dirname(sourcePath);
435
- const scriptPaths = extractReferencedScriptPaths(instructionsMarkdown);
436
- const isApproved = await requestSkillExecutionApproval({
437
- skillId: requestedSkillId,
438
- scriptPaths,
439
- context,
440
- });
441
- if (!isApproved) {
442
- return buildDeclinedResult(requestedSkillId);
1163
+ const result = await runScopedPromise;
1164
+ if (persistToolEnvelope && !parseToolExecutionEnvelopeContent(result)) {
1165
+ return wrapLoadSkillToolResult({
1166
+ result,
1167
+ preview: createTextToolPreview(truncatePreviewText(result, 1600)),
1168
+ toolCallId,
1169
+ });
443
1170
  }
444
- const scriptOutputs = await executeSkillScripts({
445
- scriptPaths,
446
- skillRoot,
447
- context,
448
- });
449
- const referenceFiles = await collectReferenceFiles(skillRoot, instructionsMarkdown);
450
- return buildSuccessResult({
451
- skillId: requestedSkillId,
452
- skillName: entry.skill_id,
453
- markdown: instructionsMarkdown,
454
- scriptOutputs,
455
- referenceFiles,
456
- });
1171
+ return result;
457
1172
  }
458
- catch (error) {
459
- if (error instanceof SkillScriptExecutionError) {
460
- throw error;
1173
+ finally {
1174
+ const inFlightResult = inFlightLoadSkillRunResults.get(runScopedResultKey);
1175
+ if (inFlightResult === runScopedPromise) {
1176
+ inFlightLoadSkillRunResults.delete(runScopedResultKey);
461
1177
  }
462
- const message = error instanceof Error ? error.message : String(error);
463
- return buildReadErrorResult(requestedSkillId, message);
464
1178
  }
465
1179
  },
466
1180
  };