@opengeni/worker-bundle 1.0.0-canary.0 → 1.0.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 (244) hide show
  1. package/dist/activities/agent-run-admission.d.ts +3 -1
  2. package/dist/activities/agent-turn/admission.d.ts +24 -3
  3. package/dist/activities/agent-turn/agent-build.d.ts +4 -0
  4. package/dist/activities/agent-turn/claude-usage-observer.d.ts +58 -0
  5. package/dist/activities/agent-turn/code-search.d.ts +60 -0
  6. package/dist/activities/agent-turn/codex-capacity.d.ts +13 -0
  7. package/dist/activities/agent-turn/codex-plan-entitlement.d.ts +105 -0
  8. package/dist/activities/agent-turn/codex.d.ts +3 -2
  9. package/dist/activities/agent-turn/compaction-prep.d.ts +21 -5
  10. package/dist/activities/agent-turn/credential-leases.d.ts +19 -33
  11. package/dist/activities/agent-turn/credential-restriction.d.ts +3 -0
  12. package/dist/activities/agent-turn/errors.d.ts +82 -11
  13. package/dist/activities/agent-turn/failure-settlement.d.ts +6 -3
  14. package/dist/activities/agent-turn/final-reply.d.ts +18 -0
  15. package/dist/activities/agent-turn/finalization-monitor.d.ts +17 -0
  16. package/dist/activities/agent-turn/finalization.d.ts +4 -2
  17. package/dist/activities/agent-turn/github-repository-bindings.d.ts +2 -0
  18. package/dist/activities/agent-turn/governance-model.d.ts +9 -2
  19. package/dist/activities/agent-turn/history-sink.d.ts +2 -0
  20. package/dist/activities/agent-turn/history.d.ts +2 -0
  21. package/dist/activities/agent-turn/input-wait-reply.d.ts +41 -0
  22. package/dist/activities/agent-turn/media-artifacts.d.ts +5 -3
  23. package/dist/activities/agent-turn/model-call-admission.d.ts +15 -0
  24. package/dist/activities/agent-turn/model-usage.d.ts +6 -0
  25. package/dist/activities/agent-turn/model-wire-dispatch.d.ts +15 -0
  26. package/dist/activities/agent-turn/optional-repositories.d.ts +59 -0
  27. package/dist/activities/agent-turn/provider-dispatch-barrier.d.ts +12 -1
  28. package/dist/activities/agent-turn/quiescence.d.ts +7 -7
  29. package/dist/activities/agent-turn/recovery-warning.d.ts +9 -0
  30. package/dist/activities/agent-turn/refresh-credentials.d.ts +6 -0
  31. package/dist/activities/agent-turn/run-credentials.d.ts +8 -3
  32. package/dist/activities/agent-turn/sandbox-establish.d.ts +2 -1
  33. package/dist/activities/agent-turn/sandbox-provision.d.ts +3 -1
  34. package/dist/activities/agent-turn/sandbox-route.d.ts +3 -20
  35. package/dist/activities/agent-turn/sandbox-runtime.d.ts +2 -2
  36. package/dist/activities/agent-turn/session-title.d.ts +34 -2
  37. package/dist/activities/agent-turn/skill-checkout.d.ts +25 -6
  38. package/dist/activities/agent-turn/skill-read.d.ts +70 -2
  39. package/dist/activities/agent-turn/skill-remove.d.ts +14 -0
  40. package/dist/activities/agent-turn/skill-search.d.ts +3 -1
  41. package/dist/activities/agent-turn/skill-tools.d.ts +15 -2
  42. package/dist/activities/agent-turn/skill-transfer.d.ts +18 -6
  43. package/dist/activities/agent-turn/stream-attempt.d.ts +7 -3
  44. package/dist/activities/agent-turn/subscription-lease.d.ts +38 -0
  45. package/dist/activities/agent-turn/tool-environment.d.ts +56 -6
  46. package/dist/activities/agent-turn/tool-policy.d.ts +43 -0
  47. package/dist/activities/agent-turn/turn-context.d.ts +29 -0
  48. package/dist/activities/agent-turn/turn-sandbox-access.d.ts +3 -0
  49. package/dist/activities/agent-turn/xai-capacity.d.ts +2 -1
  50. package/dist/activities/automations.d.ts +3 -1
  51. package/dist/activities/browser-deadline-checkpoint.d.ts +29 -0
  52. package/dist/activities/codemode-dispatcher.d.ts +9 -2
  53. package/dist/activities/codex-capacity.d.ts +18 -2
  54. package/dist/activities/codex-rotation.d.ts +7 -1
  55. package/dist/activities/context-compaction.d.ts +16 -2
  56. package/dist/activities/gmail-files.d.ts +32 -0
  57. package/dist/activities/goals.d.ts +8 -0
  58. package/dist/activities/knowledge-indexing.d.ts +19 -0
  59. package/dist/activities/mcp-account-routes.d.ts +3 -0
  60. package/dist/activities/mcp-credentials.d.ts +12 -13
  61. package/dist/activities/mcp-operation-authority.d.ts +0 -5
  62. package/dist/activities/model-call-fact-reconciler.d.ts +34 -0
  63. package/dist/activities/open-suffix-resume.d.ts +13 -0
  64. package/dist/activities/parent-wake.d.ts +6 -12
  65. package/dist/activities/retained-screenshots.d.ts +7 -2
  66. package/dist/activities/rig-verification.d.ts +2 -3
  67. package/dist/activities/run-credential-renewal.d.ts +1 -0
  68. package/dist/activities/run-credentials.d.ts +15 -3
  69. package/dist/activities/sandbox-images.d.ts +1 -1
  70. package/dist/activities/sandbox-lease.d.ts +47 -9
  71. package/dist/activities/scheduled-tasks.d.ts +4 -35
  72. package/dist/activities/session-state.d.ts +70 -3
  73. package/dist/activities/types.d.ts +55 -6
  74. package/dist/activities/workspace-capture.d.ts +2 -0
  75. package/dist/activities/workspace-credential-provider.d.ts +46 -0
  76. package/dist/{activities-control-HMFGSKC5.js → activities-control-BNPTRTOE.js} +1896 -418
  77. package/dist/activities-control-BNPTRTOE.js.map +1 -0
  78. package/dist/activities-control.d.ts +182 -4
  79. package/dist/{activities-turn-AP67JADS.js → activities-turn-5LGZOSVU.js} +4476 -1327
  80. package/dist/activities-turn-5LGZOSVU.js.map +1 -0
  81. package/dist/activities.d.ts +182 -4
  82. package/dist/artifact-materializer-entry.js +5 -2
  83. package/dist/artifact-materializer-entry.js.map +1 -1
  84. package/dist/artifact-outbox-entry.js +5 -2
  85. package/dist/artifact-outbox-entry.js.map +1 -1
  86. package/dist/browser-deadline-checkpoint-contract.d.ts +2 -0
  87. package/dist/{chunk-A4XQWVZK.js → chunk-3ZTRWDMN.js} +307 -4
  88. package/dist/chunk-3ZTRWDMN.js.map +1 -0
  89. package/dist/{chunk-RAMNYSAW.js → chunk-4ER3YXOT.js} +22 -4
  90. package/dist/chunk-4ER3YXOT.js.map +1 -0
  91. package/dist/{chunk-CYITU5UH.js → chunk-LJX7Z2KP.js} +1445 -343
  92. package/dist/chunk-LJX7Z2KP.js.map +1 -0
  93. package/dist/editable-artifact-materializer-service.d.ts +2 -2
  94. package/dist/editable-artifact-outbox-service.d.ts +2 -2
  95. package/dist/host-export-pump.d.ts +3 -1
  96. package/dist/host-export.d.ts +1 -1
  97. package/dist/host-export.js +1 -1
  98. package/dist/http.d.ts +1 -1
  99. package/dist/index.d.ts +95 -3
  100. package/dist/index.js +137 -43
  101. package/dist/index.js.map +1 -1
  102. package/dist/observability-metrics.d.ts +118 -8
  103. package/dist/retained-process-diagnostics.d.ts +4 -0
  104. package/dist/retained-process-retry.d.ts +7 -0
  105. package/dist/sandbox-resume.d.ts +55 -13
  106. package/dist/sandbox-routing.d.ts +5 -3
  107. package/dist/sandbox-snapshot-diagnostics.d.ts +20 -0
  108. package/dist/session-event-fanout.d.ts +6 -0
  109. package/dist/warm-capture-holder.d.ts +3 -0
  110. package/dist/worker-service-lifecycle.d.ts +12 -0
  111. package/dist/workflow-bundle.js +273 -41
  112. package/dist/workflows/activities.d.ts +1 -1
  113. package/dist/workflows/browser-deadline-checkpoint.d.ts +9 -0
  114. package/dist/workflows/scheduled-tasks.d.ts +4 -0
  115. package/dist/workflows/session.d.ts +36 -0
  116. package/dist/workflows.d.ts +1 -0
  117. package/package.json +21 -19
  118. package/src/activities/agent-run-admission.ts +17 -1
  119. package/src/activities/agent-turn/admission.ts +66 -3
  120. package/src/activities/agent-turn/agent-build.ts +84 -10
  121. package/src/activities/agent-turn/claim.ts +186 -43
  122. package/src/activities/agent-turn/claude-usage-observer.ts +221 -0
  123. package/src/activities/agent-turn/code-search.ts +275 -0
  124. package/src/activities/agent-turn/codex-capacity.ts +172 -8
  125. package/src/activities/agent-turn/codex-plan-entitlement.ts +254 -0
  126. package/src/activities/agent-turn/codex.ts +13 -5
  127. package/src/activities/agent-turn/compaction-prep.ts +170 -102
  128. package/src/activities/agent-turn/credential-leases.ts +122 -216
  129. package/src/activities/agent-turn/credential-restriction.ts +14 -0
  130. package/src/activities/agent-turn/errors.ts +647 -48
  131. package/src/activities/agent-turn/failure-settlement.ts +682 -130
  132. package/src/activities/agent-turn/final-reply.ts +46 -0
  133. package/src/activities/agent-turn/finalization-monitor.ts +120 -0
  134. package/src/activities/agent-turn/finalization.ts +146 -43
  135. package/src/activities/agent-turn/github-repository-bindings.ts +3 -1
  136. package/src/activities/agent-turn/governance-model.ts +69 -28
  137. package/src/activities/agent-turn/history-sink.ts +16 -2
  138. package/src/activities/agent-turn/history.ts +38 -11
  139. package/src/activities/agent-turn/input-wait-reply.ts +103 -0
  140. package/src/activities/agent-turn/knowledge-source-tools.ts +2 -0
  141. package/src/activities/agent-turn/media-artifacts.ts +2 -1
  142. package/src/activities/agent-turn/model-call-admission.ts +94 -0
  143. package/src/activities/agent-turn/model-usage.ts +64 -3
  144. package/src/activities/agent-turn/model-wire-dispatch.ts +27 -0
  145. package/src/activities/agent-turn/optional-repositories.ts +224 -0
  146. package/src/activities/agent-turn/provider-dispatch-barrier.ts +22 -0
  147. package/src/activities/agent-turn/quiescence.ts +10 -7
  148. package/src/activities/agent-turn/recovery-warning.ts +16 -0
  149. package/src/activities/agent-turn/refresh-credentials.ts +44 -0
  150. package/src/activities/agent-turn/run-credentials.ts +116 -55
  151. package/src/activities/agent-turn/run.ts +320 -93
  152. package/src/activities/agent-turn/sandbox-establish.ts +84 -8
  153. package/src/activities/agent-turn/sandbox-provision.ts +3 -1
  154. package/src/activities/agent-turn/sandbox-route.ts +4 -21
  155. package/src/activities/agent-turn/sandbox-runtime.ts +343 -18
  156. package/src/activities/agent-turn/session-title.ts +81 -5
  157. package/src/activities/agent-turn/skill-checkout.ts +111 -24
  158. package/src/activities/agent-turn/skill-install.ts +1 -1
  159. package/src/activities/agent-turn/skill-read.ts +374 -28
  160. package/src/activities/agent-turn/skill-remove.ts +46 -0
  161. package/src/activities/agent-turn/skill-save.ts +1 -1
  162. package/src/activities/agent-turn/skill-search.ts +10 -1
  163. package/src/activities/agent-turn/skill-selection.ts +4 -0
  164. package/src/activities/agent-turn/skill-tools.ts +112 -18
  165. package/src/activities/agent-turn/skill-transfer.ts +68 -23
  166. package/src/activities/agent-turn/stream-attempt.ts +313 -77
  167. package/src/activities/agent-turn/subscription-lease.ts +115 -0
  168. package/src/activities/agent-turn/tool-environment.ts +316 -167
  169. package/src/activities/agent-turn/tool-policy.ts +80 -1
  170. package/src/activities/agent-turn/turn-context.ts +35 -0
  171. package/src/activities/agent-turn/turn-sandbox-access.ts +7 -1
  172. package/src/activities/agent-turn/xai-capacity.ts +94 -51
  173. package/src/activities/automations.ts +76 -4
  174. package/src/activities/browser-deadline-checkpoint.ts +279 -0
  175. package/src/activities/capabilities.ts +108 -11
  176. package/src/activities/codemode-dispatcher.ts +36 -8
  177. package/src/activities/codex-capacity.ts +60 -25
  178. package/src/activities/codex-rotation.ts +39 -17
  179. package/src/activities/context-compaction.ts +75 -18
  180. package/src/activities/gmail-files.ts +88 -0
  181. package/src/activities/goals.ts +116 -121
  182. package/src/activities/google-drive-publication.ts +1 -3
  183. package/src/activities/image-generation-operation.ts +1 -1
  184. package/src/activities/knowledge-indexing.ts +269 -15
  185. package/src/activities/knowledge-source-sync.ts +10 -0
  186. package/src/activities/mcp-account-routes.ts +17 -0
  187. package/src/activities/mcp-credentials.ts +83 -194
  188. package/src/activities/mcp-operation-authority.ts +16 -41
  189. package/src/activities/model-call-fact-reconciler.ts +112 -0
  190. package/src/activities/open-suffix-resume.ts +15 -2
  191. package/src/activities/parent-wake.ts +46 -21
  192. package/src/activities/retained-screenshots.ts +82 -11
  193. package/src/activities/rig-verification.ts +38 -37
  194. package/src/activities/run-credential-renewal.ts +13 -1
  195. package/src/activities/run-credentials.ts +59 -17
  196. package/src/activities/run-input.ts +25 -16
  197. package/src/activities/sandbox-images.ts +0 -1
  198. package/src/activities/sandbox-lease.ts +798 -82
  199. package/src/activities/scheduled-tasks.ts +550 -178
  200. package/src/activities/session-state.ts +245 -6
  201. package/src/activities/site-auth-maintenance.ts +3 -0
  202. package/src/activities/types.ts +72 -5
  203. package/src/activities/workflow-wake.ts +2 -17
  204. package/src/activities/workspace-capture.ts +13 -2
  205. package/src/activities/workspace-credential-provider.ts +283 -0
  206. package/src/activities-control.ts +4 -0
  207. package/src/activities-turn.ts +5 -0
  208. package/src/activities.ts +13 -0
  209. package/src/activity-services.ts +16 -4
  210. package/src/browser-deadline-checkpoint-contract.ts +5 -0
  211. package/src/editable-artifact-hint-broker.ts +3 -0
  212. package/src/editable-artifact-materializer-service.ts +7 -3
  213. package/src/editable-artifact-outbox-service.ts +4 -4
  214. package/src/github-rest-mcp.ts +1 -2
  215. package/src/host-export-pump.ts +26 -4
  216. package/src/host-export.ts +3 -0
  217. package/src/http.ts +4 -2
  218. package/src/index.ts +99 -37
  219. package/src/observability-metrics.ts +510 -5
  220. package/src/personal-github-git-credentials.ts +2 -1
  221. package/src/pr-review-credentials.ts +1 -3
  222. package/src/retained-process-diagnostics.ts +50 -0
  223. package/src/retained-process-retry.ts +36 -0
  224. package/src/sandbox-resume.ts +388 -101
  225. package/src/sandbox-routing.ts +150 -25
  226. package/src/sandbox-snapshot-diagnostics.ts +112 -0
  227. package/src/session-event-fanout.ts +23 -0
  228. package/src/warm-capture-holder.ts +15 -0
  229. package/src/worker-service-lifecycle.ts +53 -0
  230. package/src/workflows/activities.ts +16 -1
  231. package/src/workflows/browser-deadline-checkpoint.ts +50 -0
  232. package/src/workflows/file-upload-reaper.ts +2 -0
  233. package/src/workflows/scheduled-tasks.ts +26 -13
  234. package/src/workflows/session.ts +242 -22
  235. package/src/workflows.ts +4 -0
  236. package/dist/activities/agent-turn/knowledge-sources.d.ts +0 -20
  237. package/dist/activities/packs.d.ts +0 -55
  238. package/dist/activities-control-HMFGSKC5.js.map +0 -1
  239. package/dist/activities-turn-AP67JADS.js.map +0 -1
  240. package/dist/chunk-A4XQWVZK.js.map +0 -1
  241. package/dist/chunk-CYITU5UH.js.map +0 -1
  242. package/dist/chunk-RAMNYSAW.js.map +0 -1
  243. package/src/activities/agent-turn/knowledge-sources.ts +0 -86
  244. package/src/activities/packs.ts +0 -166
@@ -1,10 +1,28 @@
1
1
  import type { AttemptToolDefinition } from "@opengeni/codemode";
2
- import type { SandboxChannelAService } from "@opengeni/runtime/sandbox";
3
- import type { SkillTextFile } from "@opengeni/runtime/skill-library";
4
- import { checkoutSkillDirectory, readSkillDirectory } from "./skill-transfer";
2
+ import { SKILL_READ_MAX_PATHS, type SkillTextFile } from "@opengeni/runtime/skill-library";
3
+ import { checkoutSkillDirectory, readSkillDirectory, type SkillFileSystem } from "./skill-transfer";
5
4
  import type { SkillSaveReceipt, SkillSaveRequest } from "./skill-save";
6
5
 
7
- type SkillFileSystem = Pick<SandboxChannelAService, "fsList" | "fsRead" | "fsWrite" | "fsMkdir">;
6
+ export type SkillCheckoutOutcome = "written" | "unchanged" | "refused" | "failed";
7
+
8
+ /**
9
+ * Content-free timing of one skill_checkout call. `resolve` covers authority
10
+ * and the Skill read, `sandbox` acquiring the turn's filesystem handle, and
11
+ * `write` the batched filesystem write. On a lazily provisioned turn whose
12
+ * first sandbox use is this checkout, acquiring the handle starts the box, so
13
+ * box start is inside `sandbox`. A phase that did not run is null.
14
+ */
15
+ export type SkillCheckoutObservation = Readonly<{
16
+ outcome: SkillCheckoutOutcome;
17
+ selection: "all" | "paths";
18
+ files: number;
19
+ written: number;
20
+ unchanged: number;
21
+ resolveSeconds: number | null;
22
+ sandboxSeconds: number | null;
23
+ writeSeconds: number | null;
24
+ totalSeconds: number;
25
+ }>;
8
26
 
9
27
  export function createSkillCheckoutAttemptToolDefinition(input: {
10
28
  authorize: () => Promise<void>;
@@ -14,7 +32,9 @@ export function createSkillCheckoutAttemptToolDefinition(input: {
14
32
  scopeVersion: number | null;
15
33
  files: readonly SkillTextFile[];
16
34
  }>;
17
- filesystem: () => Promise<SkillFileSystem>;
35
+ filesystem: () => Promise<Pick<SkillFileSystem, "fsWriteFiles">>;
36
+ /** Telemetry only; a failure here never changes the result. */
37
+ observe?: (observation: SkillCheckoutObservation) => void;
18
38
  }): AttemptToolDefinition {
19
39
  return {
20
40
  identity: { serverId: "opengeni", toolName: "skill_checkout" },
@@ -22,12 +42,19 @@ export function createSkillCheckoutAttemptToolDefinition(input: {
22
42
  codemodePath: ["opengeni", "skill_checkout"],
23
43
  title: "Check out Skill files",
24
44
  description:
25
- "Copy a Skill to a fresh sandbox-relative directory for execution or larger edits. Starts the sandbox if needed. Never overwrites an existing directory and does not publish changes. Retain the returned revision and scope version for skill_publish. Use skill_read for ordinary reading without a sandbox.",
45
+ "Copy Skill files into a sandbox-relative directory to run scripts or make larger edits. Starts the sandbox if needed. Writes all missing files in one step and never overwrites: files that already hold the same content are kept, and a different existing file fails the call, so repeating a checkout into the same directory is fast. Pass paths to copy only those files, for example one script to run. Only a complete copy that creates a new directory returns revisionId and scopeVersion for skill_publish. Does not publish changes. Use skill_read for reading without a sandbox.",
26
46
  inputSchema: {
27
47
  type: "object",
28
48
  properties: {
29
49
  skill: { type: "string", minLength: 1, maxLength: 512 },
30
50
  directory: { type: "string", minLength: 1, maxLength: 1024 },
51
+ paths: {
52
+ type: "array",
53
+ minItems: 1,
54
+ maxItems: SKILL_READ_MAX_PATHS,
55
+ uniqueItems: true,
56
+ items: { type: "string", minLength: 1, maxLength: 1024 },
57
+ },
31
58
  },
32
59
  required: ["skill", "directory"],
33
60
  additionalProperties: false,
@@ -35,37 +62,97 @@ export function createSkillCheckoutAttemptToolDefinition(input: {
35
62
  annotations: {
36
63
  readOnlyHint: false,
37
64
  destructiveHint: false,
38
- idempotentHint: false,
65
+ idempotentHint: true,
39
66
  openWorldHint: false,
40
67
  },
41
68
  source: "opengeni",
42
69
  approval: "none",
43
70
  execute: async (args) => {
44
- await input.authorize();
45
- const skill = await input.load(args.skill as string);
46
- const copied = await checkoutSkillDirectory(
47
- await input.filesystem(),
48
- args.directory as string,
49
- skill.files,
50
- );
51
- const output = {
52
- ...copied,
53
- skillId: skill.skillId,
54
- revisionId: skill.revisionId,
55
- scopeVersion: skill.scopeVersion,
71
+ if (
72
+ typeof args.skill !== "string" ||
73
+ typeof args.directory !== "string" ||
74
+ (args.paths !== undefined &&
75
+ (!Array.isArray(args.paths) || args.paths.some((path) => typeof path !== "string")))
76
+ ) {
77
+ throw new Error(
78
+ "skill_checkout requires a Skill, a directory, and optional relative paths.",
79
+ );
80
+ }
81
+ const paths = args.paths as string[] | undefined;
82
+ const started = performance.now();
83
+ const phases: { resolve: number | null; sandbox: number | null; write: number | null } = {
84
+ resolve: null,
85
+ sandbox: null,
86
+ write: null,
56
87
  };
57
- return {
58
- isError: false,
59
- content: [{ type: "text", text: JSON.stringify(output) }],
60
- structuredContent: output,
88
+ const timed = async <T>(phase: keyof typeof phases, run: () => Promise<T>): Promise<T> => {
89
+ const phaseStarted = performance.now();
90
+ try {
91
+ return await run();
92
+ } finally {
93
+ phases[phase] = (performance.now() - phaseStarted) / 1_000;
94
+ }
61
95
  };
96
+ let copied: Awaited<ReturnType<typeof checkoutSkillDirectory>> | undefined;
97
+ let outcome: SkillCheckoutOutcome = "refused";
98
+ try {
99
+ const skill = await timed("resolve", async () => {
100
+ await input.authorize();
101
+ return input.load(args.skill as string);
102
+ });
103
+ outcome = "failed";
104
+ const filesystem = await timed("sandbox", input.filesystem);
105
+ copied = await timed("write", () =>
106
+ checkoutSkillDirectory(
107
+ filesystem,
108
+ args.directory as string,
109
+ skill.files,
110
+ paths ? { paths } : {},
111
+ ),
112
+ );
113
+ outcome = copied.written > 0 ? "written" : "unchanged";
114
+ // Only a complete copy into a directory this call created mirrors the
115
+ // revision exactly, so only it may serve as a skill_publish base.
116
+ const publishable = !paths && copied.createdDirectory;
117
+ const output = {
118
+ directory: copied.directory,
119
+ fileCount: copied.fileCount,
120
+ written: copied.written,
121
+ unchanged: copied.unchanged,
122
+ skillId: skill.skillId,
123
+ ...(publishable
124
+ ? { revisionId: skill.revisionId, scopeVersion: skill.scopeVersion }
125
+ : { publishable: false }),
126
+ };
127
+ return {
128
+ isError: false,
129
+ content: [{ type: "text", text: JSON.stringify(output) }],
130
+ structuredContent: output,
131
+ };
132
+ } finally {
133
+ try {
134
+ input.observe?.({
135
+ outcome,
136
+ selection: paths ? "paths" : "all",
137
+ files: copied?.fileCount ?? 0,
138
+ written: copied?.written ?? 0,
139
+ unchanged: copied?.unchanged ?? 0,
140
+ resolveSeconds: phases.resolve,
141
+ sandboxSeconds: phases.sandbox,
142
+ writeSeconds: phases.write,
143
+ totalSeconds: (performance.now() - started) / 1_000,
144
+ });
145
+ } catch {
146
+ // Telemetry is best effort.
147
+ }
148
+ }
62
149
  },
63
150
  };
64
151
  }
65
152
 
66
153
  export function createSkillPublishAttemptToolDefinition(input: {
67
154
  authorize: () => Promise<void>;
68
- filesystem: () => Promise<SkillFileSystem>;
155
+ filesystem: () => Promise<Pick<SkillFileSystem, "fsList" | "fsRead">>;
69
156
  save: (request: SkillSaveRequest) => Promise<SkillSaveReceipt>;
70
157
  }): AttemptToolDefinition {
71
158
  return {
@@ -17,7 +17,7 @@ export function createSkillInstallAttemptToolDefinition(input: {
17
17
  codemodePath: ["opengeni", "skill_install"],
18
18
  title: "Install a Skill",
19
19
  description:
20
- "Install a Skill from a public skills.sh or GitHub URL, or library:<id> returned by skill_search. The server resolves and pins the source; no prior preview or invented hash is required. The effective Skills setting governs agent installation: Automatic publishes, Review first leaves a pending change in Knowledge > Needs review while the task continues, and Off prevents agent installation. Human installation from the UI remains available. Do not ask an approval question for a pending receipt. Replacing an existing installation requires its current installation version, and preserves workspace customizations.",
20
+ "Install a Skill from a public skills.sh or GitHub URL, such as a public skill_search result, or a curated Opengeni library entry as library:<id>. The server resolves and pins the source; no prior preview or invented hash is required. The effective Skills setting governs agent installation: Automatic publishes, Review first leaves a pending change in Knowledge > Needs review while the task continues, and Off prevents agent installation. Human installation from the UI remains available. Do not ask an approval question for a pending receipt. Replacing an existing installation requires its current installation version, and preserves workspace customizations.",
21
21
  inputSchema: {
22
22
  type: "object",
23
23
  properties: {
@@ -1,26 +1,183 @@
1
- import type { AttemptToolDefinition } from "@opengeni/codemode";
1
+ import { boundModelToolOutputItem } from "@opengeni/codex";
2
+ import type { AttemptToolDefinition, AttemptToolExecutionContext } from "@opengeni/codemode";
3
+ import {
4
+ SKILL_USE_META_KEY,
5
+ SkillUse,
6
+ type AttemptToolResult,
7
+ type SkillReadKind,
8
+ type SkillUseSource,
9
+ } from "@opengeni/contracts";
10
+ import { modelToolResultFits } from "@opengeni/runtime";
2
11
  import {
3
12
  listSkillPaths,
4
13
  PORTABLE_SKILL_MAX_FILES,
5
14
  readSkillFiles,
15
+ skillScriptIndex,
6
16
  SKILL_READ_MAX_PATHS,
7
17
  type SkillTextFile,
8
18
  } from "@opengeni/runtime/skill-library";
9
19
 
10
20
  export const SKILL_READ_TOOL_NAME = "skill_read";
11
21
 
22
+ type JsonValue = NonNullable<AttemptToolResult["_meta"]>[string];
23
+
24
+ /** Content-free provenance of a resolved Skill. The caller never receives it. */
25
+ export type SkillReadOrigin = Readonly<{
26
+ id: string;
27
+ source: SkillUseSource;
28
+ /** Platform display label of a built-in; never user-authored text. */
29
+ label?: string;
30
+ /** Whole-artifact digest, for an artifact without a ledger revision. */
31
+ contentSha256?: string;
32
+ }>;
33
+
12
34
  export type SkillReadContent = Readonly<{
13
35
  files: readonly SkillTextFile[];
14
36
  skillId: string;
15
37
  revisionId: string;
16
38
  scopeVersion: number;
17
39
  installationVersion?: number;
40
+ origin?: SkillReadOrigin;
41
+ }>;
42
+
43
+ /** A selected built-in or session artifact: no ledger identity, and none is synthesized. */
44
+ export type SelectedSkillReadContent = Readonly<{
45
+ files: readonly SkillTextFile[];
46
+ origin: SkillReadOrigin;
47
+ }>;
48
+
49
+ type SkillReadIdentity = Partial<
50
+ Pick<SkillReadContent, "skillId" | "revisionId" | "scopeVersion" | "installationVersion">
51
+ >;
52
+
53
+ export type SkillReadObservation = Readonly<{
54
+ caller: AttemptToolExecutionContext["caller"]["kind"];
55
+ kind: SkillReadKind;
56
+ /** Null when the read was refused before a Skill resolved. */
57
+ source: SkillUseSource | null;
58
+ /** The resolved Skill id, or the requested identifier when none resolved. */
59
+ skill: string;
60
+ }>;
61
+
62
+ /**
63
+ * Content-free read telemetry. It never changes what the caller receives: a
64
+ * model result gains only MCP `_meta`, which the model never sees and which
65
+ * only the tool-output event projection keeps.
66
+ */
67
+ export type SkillReadTelemetry = Readonly<{
68
+ /** Ids in the Skill index the model sees this turn; null until known. */
69
+ indexedSkillIds: () => ReadonlySet<string> | null;
70
+ /** Whether skill_search returned this id earlier in this turn attempt. */
71
+ searched: (id: string) => boolean;
72
+ observe: (observation: SkillReadObservation) => void;
73
+ }>;
74
+
75
+ export type SkillReadActiveHistory = Readonly<{
76
+ /** Earlier skill_read results still in this session's active model history. */
77
+ readResults: () => Promise<ReadonlyArray<Record<string, unknown>>>;
78
+ /**
79
+ * This turn's model tool-output bound. Rows are bounded when stored, but a
80
+ * later model with a lower bound receives a shorter copy than the row holds.
81
+ */
82
+ toolOutputTruncationTokens: () => number;
83
+ /** The lookup only saves tokens, so a failure returns the full text. */
84
+ onLookupFailed?: (error: unknown) => void;
18
85
  }>;
19
86
 
87
+ const IDENTITY_KEYS = ["skillId", "revisionId", "scopeVersion", "installationVersion"] as const;
88
+
89
+ export const SKILL_UNAVAILABLE_MESSAGE = "Skill is not available in this session.";
90
+ /** Bounds on the Skill list an unresolved identifier returns. */
91
+ export const SKILL_UNAVAILABLE_LIST_MAX_ENTRIES = 25;
92
+ export const SKILL_UNAVAILABLE_LIST_MAX_BYTES = 4 * 1024;
93
+ // A shorter shared edge is chance: random hex ids share 1-3 leading characters often.
94
+ const MIN_RESEMBLANCE = 4;
95
+
96
+ export type AvailableSkill = Readonly<{ id: string; name: string }>;
97
+
98
+ /**
99
+ * The error for an identifier that resolves to no Skill. It lists available
100
+ * Skills by id and name only, the descriptors the Skill index and skill_search
101
+ * already show the model, so the caller can retry instead of rediscovering
102
+ * the work from scratch. Entries resembling the requested identifier (shared
103
+ * prefix, suffix, or containment) come first, so a mistyped or spliced id
104
+ * keeps its likely targets inside the bound. The requested identifier is
105
+ * never echoed.
106
+ */
107
+ export function unavailableSkillError(
108
+ requested: string,
109
+ available: readonly AvailableSkill[],
110
+ ): Error {
111
+ const byId = new Map<string, AvailableSkill>();
112
+ for (const entry of available) if (!byId.has(entry.id)) byId.set(entry.id, entry);
113
+ const wanted = requested.toLowerCase();
114
+ const ranked = [...byId.values()]
115
+ .map((entry) => ({ entry, score: resemblance(wanted, entry) }))
116
+ .sort(
117
+ (a, b) =>
118
+ b.score - a.score || compare(a.entry.name, b.entry.name) || compare(a.entry.id, b.entry.id),
119
+ );
120
+ const header = [
121
+ SKILL_UNAVAILABLE_MESSAGE,
122
+ ...(wanted.startsWith("repository:")
123
+ ? ["Repository Skills are read with repository_skill_read."]
124
+ : []),
125
+ ranked.length === 0
126
+ ? "No configured Skills are available in this session."
127
+ : "Retry with an exact id from these available Skills:",
128
+ ].join("\n");
129
+ const lines = [header];
130
+ // Reserve room for the omission notice so the whole message stays bounded.
131
+ const budget = SKILL_UNAVAILABLE_LIST_MAX_BYTES - 128;
132
+ let bytes = Buffer.byteLength(header, "utf8");
133
+ let listed = 0;
134
+ for (const { entry } of ranked) {
135
+ const line = `- ${JSON.stringify({ id: entry.id, name: entry.name })}`;
136
+ const lineBytes = Buffer.byteLength(line, "utf8") + 1;
137
+ if (listed >= SKILL_UNAVAILABLE_LIST_MAX_ENTRIES || bytes + lineBytes > budget) continue;
138
+ lines.push(line);
139
+ bytes += lineBytes;
140
+ listed += 1;
141
+ }
142
+ const omitted = ranked.length - listed;
143
+ if (omitted > 0)
144
+ lines.push(`${omitted} more available Skills are not listed. Use skill_search to find them.`);
145
+ return new Error(lines.join("\n"));
146
+ }
147
+
148
+ function resemblance(wanted: string, entry: AvailableSkill): number {
149
+ let best = 0;
150
+ for (const key of [entry.id.toLowerCase(), entry.name.toLowerCase()]) {
151
+ const contained =
152
+ wanted.includes(key) || key.includes(wanted) ? Math.min(wanted.length, key.length) : 0;
153
+ let prefix = 0;
154
+ while (prefix < wanted.length && prefix < key.length && wanted[prefix] === key[prefix])
155
+ prefix += 1;
156
+ let suffix = 0;
157
+ while (
158
+ suffix < wanted.length &&
159
+ suffix < key.length &&
160
+ wanted[wanted.length - 1 - suffix] === key[key.length - 1 - suffix]
161
+ )
162
+ suffix += 1;
163
+ best = Math.max(best, contained, prefix, suffix);
164
+ }
165
+ return best >= MIN_RESEMBLANCE ? best : 0;
166
+ }
167
+
168
+ function compare(left: string, right: string): number {
169
+ return left < right ? -1 : left > right ? 1 : 0;
170
+ }
171
+
20
172
  /** A first-party gateway definition, not a sandbox capability or second backend. */
21
173
  export function createSkillReadAttemptToolDefinition(input: {
22
174
  authorize: () => Promise<void>;
23
- load: (skill: string) => Promise<readonly SkillTextFile[] | SkillReadContent>;
175
+ load: (
176
+ skill: string,
177
+ ) => Promise<readonly SkillTextFile[] | SkillReadContent | SelectedSkillReadContent>;
178
+ /** Without it every read returns full text. */
179
+ activeHistory?: SkillReadActiveHistory;
180
+ telemetry?: SkillReadTelemetry;
24
181
  }): AttemptToolDefinition {
25
182
  return {
26
183
  identity: { serverId: "opengeni", toolName: SKILL_READ_TOOL_NAME },
@@ -54,7 +211,7 @@ export function createSkillReadAttemptToolDefinition(input: {
54
211
  },
55
212
  source: "opengeni",
56
213
  approval: "none",
57
- execute: async (args) => {
214
+ execute: async (args, context) => {
58
215
  if (
59
216
  typeof args.skill !== "string" ||
60
217
  !args.skill ||
@@ -68,31 +225,220 @@ export function createSkillReadAttemptToolDefinition(input: {
68
225
  throw new Error("skill_read listFiles:true cannot be combined with paths.");
69
226
  }
70
227
  await input.authorize();
71
- const loaded = await input.load(args.skill);
72
- const metadata = "files" in loaded ? loaded : null;
73
- const files = metadata ? metadata.files : (loaded as readonly SkillTextFile[]);
74
- const selected =
75
- args.listFiles === true
76
- ? listSkillPaths(files, PORTABLE_SKILL_MAX_FILES)
77
- : readSkillFiles(files, args.paths as string[] | undefined);
78
- const output = {
79
- ...(metadata
80
- ? {
81
- skillId: metadata.skillId,
82
- revisionId: metadata.revisionId,
83
- scopeVersion: metadata.scopeVersion,
84
- ...(metadata.installationVersion !== undefined
85
- ? { installationVersion: metadata.installationVersion }
86
- : {}),
87
- }
88
- : {}),
89
- ...selected,
90
- };
91
- return {
92
- isError: false,
93
- content: [{ type: "text", text: JSON.stringify(output) }],
94
- structuredContent: output,
95
- };
228
+ const skill = args.skill;
229
+ let origin: SkillReadOrigin | undefined;
230
+ let identity: SkillReadIdentity = {};
231
+ let read: { kind: Exclude<SkillReadKind, "refused">; result: AttemptToolResult };
232
+ try {
233
+ const loaded = await input.load(skill);
234
+ const content = "files" in loaded ? loaded : { files: loaded };
235
+ origin = "origin" in content ? content.origin : undefined;
236
+ if ("skillId" in content) {
237
+ identity = {
238
+ skillId: content.skillId,
239
+ revisionId: content.revisionId,
240
+ scopeVersion: content.scopeVersion,
241
+ ...(content.installationVersion !== undefined
242
+ ? { installationVersion: content.installationVersion }
243
+ : {}),
244
+ };
245
+ }
246
+ read = await readResult(
247
+ content.files,
248
+ identity,
249
+ args as { skill: string; listFiles?: boolean; paths?: string[] },
250
+ context,
251
+ input.activeHistory,
252
+ );
253
+ } catch (error) {
254
+ observe(input.telemetry, { caller: context.caller.kind, kind: "refused", origin, skill });
255
+ throw error;
256
+ }
257
+ observe(input.telemetry, { caller: context.caller.kind, kind: read.kind, origin, skill });
258
+ return context.caller.kind === "model" && origin && input.telemetry
259
+ ? withSkillUse(read, origin, identity, input.telemetry)
260
+ : read.result;
96
261
  },
97
262
  };
98
263
  }
264
+
265
+ async function readResult(
266
+ files: readonly SkillTextFile[],
267
+ identity: SkillReadIdentity,
268
+ args: { skill: string; listFiles?: boolean; paths?: string[] },
269
+ context: AttemptToolExecutionContext,
270
+ activeHistory: SkillReadActiveHistory | undefined,
271
+ ): Promise<{ kind: Exclude<SkillReadKind, "refused">; result: AttemptToolResult }> {
272
+ if (args.listFiles === true) {
273
+ return {
274
+ kind: "list",
275
+ result: textResult({ ...identity, ...listSkillPaths(files, PORTABLE_SKILL_MAX_FILES) }),
276
+ };
277
+ }
278
+ const selected = readSkillFiles(files, args.paths);
279
+ // Only the model's default SKILL.md read is deduplicated: explicit paths
280
+ // are the fresh-copy request, and a Codemode program never sees history.
281
+ if (
282
+ args.paths === undefined &&
283
+ context.caller.kind === "model" &&
284
+ activeHistory &&
285
+ (await inActiveHistory(activeHistory, identity, selected.files[0]!))
286
+ ) {
287
+ return {
288
+ kind: "already_in_context",
289
+ result: textResult({
290
+ ...identity,
291
+ alreadyInContext: true,
292
+ message: `Already in context: SKILL.md of ${JSON.stringify(args.skill)}${identity.revisionId ? ` revision ${identity.revisionId}` : ""} was returned earlier in this conversation and is unchanged. Use that copy. Re-read only if you need a fresh copy: call skill_read with paths ["SKILL.md"].`,
293
+ }),
294
+ };
295
+ }
296
+ if (args.paths !== undefined) {
297
+ return { kind: "files", result: textResult({ ...identity, ...selected }) };
298
+ }
299
+ // The default read also indexes runnable files (path and first usage line)
300
+ // so the commands are visible without a sandbox checkout.
301
+ const scripts = skillScriptIndex(files);
302
+ return {
303
+ kind: "full",
304
+ result: textResult({ ...identity, ...selected, ...(scripts ?? {}) }),
305
+ };
306
+ }
307
+
308
+ /** Telemetry must never change or fail a read. */
309
+ function observe(
310
+ telemetry: SkillReadTelemetry | undefined,
311
+ read: {
312
+ caller: SkillReadObservation["caller"];
313
+ kind: SkillReadKind;
314
+ origin: SkillReadOrigin | undefined;
315
+ skill: string;
316
+ },
317
+ ): void {
318
+ if (!telemetry) return;
319
+ try {
320
+ telemetry.observe({
321
+ caller: read.caller,
322
+ kind: read.kind,
323
+ source: read.origin?.source ?? null,
324
+ skill: read.origin?.id ?? read.skill,
325
+ });
326
+ } catch {
327
+ // Metrics are best effort.
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Adds the content-free Skill-use fact as MCP `_meta`. The model output is
333
+ * the text part alone, so it stays byte-identical. The fact is dropped
334
+ * rather than let it push a result past the model-visible size cap, which
335
+ * would change what the model receives.
336
+ */
337
+ function withSkillUse(
338
+ read: { kind: Exclude<SkillReadKind, "refused">; result: AttemptToolResult },
339
+ origin: SkillReadOrigin,
340
+ identity: SkillReadIdentity,
341
+ telemetry: SkillReadTelemetry,
342
+ ): AttemptToolResult {
343
+ try {
344
+ const text = read.result.content[0]?.type === "text" ? read.result.content[0].text : "";
345
+ // Parsing rejects anything outside the closed, content-free shape.
346
+ const use = SkillUse.parse({
347
+ id: origin.id,
348
+ source: origin.source,
349
+ ...(origin.label ? { label: origin.label } : {}),
350
+ ...(identity.revisionId
351
+ ? { revisionId: identity.revisionId }
352
+ : origin.contentSha256
353
+ ? { contentSha256: origin.contentSha256 }
354
+ : {}),
355
+ kind: read.kind,
356
+ bytes: Buffer.byteLength(text, "utf8"),
357
+ inIndex: telemetry.indexedSkillIds()?.has(origin.id) ?? false,
358
+ searchedThisTurn: telemetry.searched(origin.id),
359
+ });
360
+ const annotated: AttemptToolResult = {
361
+ ...read.result,
362
+ _meta: { ...read.result._meta, [SKILL_USE_META_KEY]: use as JsonValue },
363
+ };
364
+ return modelToolResultFits(annotated) ? annotated : read.result;
365
+ } catch {
366
+ return read.result;
367
+ }
368
+ }
369
+
370
+ function textResult(
371
+ output: NonNullable<AttemptToolResult["structuredContent"]>,
372
+ ): AttemptToolResult {
373
+ return {
374
+ isError: false,
375
+ content: [{ type: "text", text: JSON.stringify(output) }],
376
+ structuredContent: output,
377
+ };
378
+ }
379
+
380
+ async function inActiveHistory(
381
+ history: SkillReadActiveHistory,
382
+ identity: SkillReadIdentity,
383
+ file: SkillTextFile,
384
+ ): Promise<boolean> {
385
+ let results: ReadonlyArray<Record<string, unknown>>;
386
+ try {
387
+ results = await history.readResults();
388
+ } catch (error) {
389
+ history.onLookupFailed?.(error);
390
+ return false;
391
+ }
392
+ const tokens = history.toolOutputTruncationTokens();
393
+ // Judge the copy this turn's model receives, not the stored row.
394
+ return results.some((item) =>
395
+ returnedFile(boundModelToolOutputItem(item, tokens), identity, file),
396
+ );
397
+ }
398
+
399
+ /**
400
+ * True when an earlier result gave the model this exact file text under the
401
+ * same Skill identity. A truncated, spilled, or failed result does not parse
402
+ * as a Skill read, so it never counts.
403
+ */
404
+ function returnedFile(
405
+ item: Record<string, unknown>,
406
+ identity: SkillReadIdentity,
407
+ file: SkillTextFile,
408
+ ): boolean {
409
+ const text = resultText(item.output);
410
+ if (text === null) return false;
411
+ let previous: unknown;
412
+ try {
413
+ previous = JSON.parse(text);
414
+ } catch {
415
+ return false;
416
+ }
417
+ if (!previous || typeof previous !== "object" || Array.isArray(previous)) return false;
418
+ const record = previous as Record<string, unknown>;
419
+ return (
420
+ IDENTITY_KEYS.every((key) => record[key] === identity[key]) &&
421
+ Array.isArray(record.files) &&
422
+ record.files.some(
423
+ (candidate: unknown) =>
424
+ !!candidate &&
425
+ typeof candidate === "object" &&
426
+ (candidate as SkillTextFile).path === file.path &&
427
+ (candidate as SkillTextFile).content === file.content,
428
+ )
429
+ );
430
+ }
431
+
432
+ /** The single text part of a function result, in any SDK output shape. */
433
+ function resultText(output: unknown): string | null {
434
+ if (typeof output === "string") return output;
435
+ const parts = Array.isArray(output) ? output : [output];
436
+ if (parts.length !== 1) return null;
437
+ const part = parts[0] as { type?: unknown; text?: unknown } | null;
438
+ return part &&
439
+ typeof part === "object" &&
440
+ (part.type === "text" || part.type === "input_text") &&
441
+ typeof part.text === "string"
442
+ ? part.text
443
+ : null;
444
+ }
@@ -0,0 +1,46 @@
1
+ import type { AttemptToolDefinition } from "@opengeni/codemode";
2
+ import { z } from "zod";
3
+ import type { SkillWriteReceipt } from "@opengeni/contracts";
4
+
5
+ export const SkillRemoveRequest = z
6
+ .object({
7
+ operationId: z.uuid(),
8
+ skillId: z.uuid(),
9
+ expectedRevisionId: z.uuid().nullable(),
10
+ expectedScopeVersion: z.number().int().positive(),
11
+ reason: z.string().min(1).max(2000),
12
+ })
13
+ .strict();
14
+
15
+ export function createSkillRemoveAttemptToolDefinition(input: {
16
+ authorize: () => Promise<void>;
17
+ remove: (request: z.infer<typeof SkillRemoveRequest>) => Promise<SkillWriteReceipt>;
18
+ }): AttemptToolDefinition {
19
+ return {
20
+ identity: { serverId: "opengeni", toolName: "skill_remove" },
21
+ modelName: "skill_remove",
22
+ codemodePath: ["opengeni", "skill_remove"],
23
+ title: "Permanently remove Skill",
24
+ description:
25
+ "Permanently delete a saved Skill and ALL its stored revisions, not a recoverable hide. Conversations remain unchanged. Read the exact Skill first; supply its current active revision and scope version. Built-in, repository and inline session Skills cannot be removed. Automatic applies deletion; Review first retains an explicitly labeled deletion proposal for human review in Knowledge > Needs review; Off refuses changes. Reuse the same operationId and exact arguments after an uncertain result. Never report a pending proposal as deleted.",
26
+ inputSchema: JSON.parse(JSON.stringify(z.toJSONSchema(SkillRemoveRequest))),
27
+ annotations: {
28
+ readOnlyHint: false,
29
+ destructiveHint: true,
30
+ idempotentHint: true,
31
+ openWorldHint: false,
32
+ },
33
+ source: "opengeni",
34
+ approval: "none",
35
+ execute: async (args) => {
36
+ await input.authorize();
37
+ const output = await input.remove(SkillRemoveRequest.parse(args));
38
+ const text = JSON.stringify(output);
39
+ return {
40
+ isError: false,
41
+ content: [{ type: "text", text }],
42
+ structuredContent: JSON.parse(text),
43
+ };
44
+ },
45
+ };
46
+ }