@cursor/july 0.1.24 → 0.1.26

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 (205) hide show
  1. package/dist/bin/agent-serve.js +0 -0
  2. package/dist/channels/github/instrument.d.ts +20 -0
  3. package/dist/channels/github/instrument.d.ts.map +1 -0
  4. package/dist/docs/404.html +1 -1
  5. package/dist/docs/ab.html +2 -2
  6. package/dist/docs/assets/{app.CvSGaAxk.js → app.DYcC9FY-.js} +1 -1
  7. package/dist/docs/assets/chunks/@localSearchIndexroot.BQTzJjR_.js +1 -0
  8. package/dist/docs/assets/chunks/{VPLocalSearchBox.J1jJbCvs.js → VPLocalSearchBox.o4N_knTV.js} +1 -1
  9. package/dist/docs/assets/chunks/{theme.C7vfzr7h.js → theme.DQ-njyo0.js} +2 -2
  10. package/dist/docs/building-with-agents.html +2 -2
  11. package/dist/docs/concepts.html +2 -2
  12. package/dist/docs/deployment.html +2 -2
  13. package/dist/docs/evals.html +2 -2
  14. package/dist/docs/example-agents/approval-buddy.html +2 -2
  15. package/dist/docs/example-agents/benny.html +2 -2
  16. package/dist/docs/example-agents/bugbot.html +2 -2
  17. package/dist/docs/example-agents/codebase-wiki.html +2 -2
  18. package/dist/docs/example-agents/codeowners-review.html +2 -2
  19. package/dist/docs/example-agents/concierge.html +2 -2
  20. package/dist/docs/example-agents/fsd.html +2 -2
  21. package/dist/docs/example-agents/index.html +2 -2
  22. package/dist/docs/example-agents/knowledge-base.html +2 -2
  23. package/dist/docs/example-agents/oncall.html +2 -2
  24. package/dist/docs/example-agents/security-reviewer.html +2 -2
  25. package/dist/docs/example-agents/slack-agent.html +2 -2
  26. package/dist/docs/example-agents/weather-agent.html +2 -2
  27. package/dist/docs/guides/agent-to-agent.html +2 -2
  28. package/dist/docs/guides/cloud-runtime.html +2 -2
  29. package/dist/docs/guides/github.html +2 -2
  30. package/dist/docs/guides/human-in-the-loop.html +2 -2
  31. package/dist/docs/guides/mcp-oauth.html +2 -2
  32. package/dist/docs/guides/slack.html +2 -2
  33. package/dist/docs/guides/webhooks.html +2 -2
  34. package/dist/docs/hillclimbing.html +2 -2
  35. package/dist/docs/index.html +2 -2
  36. package/dist/docs/quickstart.html +2 -2
  37. package/dist/docs/reference/agent-config.html +2 -2
  38. package/dist/docs/reference/channels.html +2 -2
  39. package/dist/docs/reference/cli.html +2 -2
  40. package/dist/docs/reference/connections.html +2 -2
  41. package/dist/docs/reference/hooks.html +2 -2
  42. package/dist/docs/reference/http-api.html +2 -2
  43. package/dist/docs/reference/instructions.html +2 -2
  44. package/dist/docs/reference/playground.html +2 -2
  45. package/dist/docs/reference/project-layout.html +2 -2
  46. package/dist/docs/reference/prompt.html +2 -2
  47. package/dist/docs/reference/schedules.html +2 -2
  48. package/dist/docs/reference/sessions.html +2 -2
  49. package/dist/docs/reference/skills.html +2 -2
  50. package/dist/docs/reference/subagents.html +2 -2
  51. package/dist/docs/reference/tools.html +2 -2
  52. package/dist/docs/scaffolding-agents.html +2 -2
  53. package/dist/docs/storage.html +2 -2
  54. package/dist/docs/troubleshooting.html +2 -2
  55. package/dist/internal/json-dir-store.d.ts +32 -0
  56. package/dist/internal/json-dir-store.d.ts.map +1 -0
  57. package/dist/internal/json-dir-store.js +100 -0
  58. package/dist/playground/assets/index-CjOQ4hN9.css +1 -0
  59. package/dist/playground/assets/{index-DTG9OsPV.js → index-DqXdAFGa.js} +41 -41
  60. package/dist/playground/index.html +2 -2
  61. package/package.json +31 -24
  62. package/src/artifacts.ts +78 -0
  63. package/src/bin/agent-serve.version.test.ts +64 -0
  64. package/src/channels/github/api.test.ts +64 -0
  65. package/src/channels/github/auth.test.ts +105 -0
  66. package/src/channels/github/cursor-account.test.ts +204 -0
  67. package/src/channels/github/forward.test.ts +457 -0
  68. package/src/channels/github/github-channel.ts +19 -2
  69. package/src/channels/github/github.test.ts +985 -0
  70. package/src/channels/github/replay.test.ts +179 -0
  71. package/src/channels/github/types.ts +6 -0
  72. package/src/channels/slack/api.post-message.test.ts +148 -0
  73. package/src/channels/slack/api.ts +3 -1
  74. package/src/channels/slack/approvals.test.ts +328 -0
  75. package/src/channels/slack/block-actions.test.ts +469 -0
  76. package/src/channels/slack/bot-mentions.test.ts +267 -0
  77. package/src/channels/slack/channel-watch.test.ts +363 -0
  78. package/src/channels/slack/cursor-account.test.ts +301 -0
  79. package/src/channels/slack/cursor-account.ts +3 -1
  80. package/src/channels/slack/defaults.final-post.test.ts +182 -0
  81. package/src/channels/slack/dispatch.test.ts +875 -0
  82. package/src/channels/slack/dispatch.ts +12 -1
  83. package/src/channels/slack/eval-directive.test.ts +273 -0
  84. package/src/channels/slack/interactive.ts +3 -0
  85. package/src/channels/slack/message-body.test.ts +54 -0
  86. package/src/channels/slack/nudge-store.test.ts +143 -0
  87. package/src/channels/slack/slack-channel.ts +6 -0
  88. package/src/channels/slack/slack.test.ts +391 -0
  89. package/src/channels/slack/stop.test.ts +23 -0
  90. package/src/channels/slack/thread-context.test.ts +202 -0
  91. package/src/channels/slack/types.ts +6 -0
  92. package/src/evals/assertions.test.ts +617 -0
  93. package/src/evals/assertions.ts +37 -1
  94. package/src/evals/expect.test.ts +144 -0
  95. package/src/evals/judge.test.ts +181 -0
  96. package/src/evals/loaders.test.ts +132 -0
  97. package/src/evals/matchers.test.ts +95 -0
  98. package/src/evals/reporters.test.ts +303 -0
  99. package/src/evals/run-facts.test.ts +259 -0
  100. package/src/evals.ts +1 -0
  101. package/src/index.ts +1 -0
  102. package/src/internal/ab-snapshot.test.ts +325 -0
  103. package/src/internal/approval-gate.test.ts +49 -0
  104. package/src/internal/approvals.integration.test.ts +383 -0
  105. package/src/internal/artifacts-store.test.ts +283 -0
  106. package/src/internal/artifacts-store.ts +290 -0
  107. package/src/internal/authored-loaders.test.ts +31 -0
  108. package/src/internal/builtin-tools/artifacts.test.ts +247 -0
  109. package/src/internal/builtin-tools/artifacts.ts +74 -0
  110. package/src/internal/builtin-tools/index.ts +21 -0
  111. package/src/internal/builtin-tools/reminders.test.ts +201 -0
  112. package/src/internal/channel-route-schema.test.ts +294 -0
  113. package/src/internal/chat-attach.test.ts +262 -0
  114. package/src/internal/cli-deploy.test.ts +1991 -0
  115. package/src/internal/cli-docs.test.ts +161 -0
  116. package/src/internal/cli-mcp.test.ts +789 -0
  117. package/src/internal/cli-skills.test.ts +133 -0
  118. package/src/internal/cli-slack.test.ts +1654 -0
  119. package/src/internal/cli-slack.ts +13 -4
  120. package/src/internal/cloud-merge.test.ts +74 -0
  121. package/src/internal/cron.test.ts +22 -0
  122. package/src/internal/cursor/account-mcp.test.ts +807 -0
  123. package/src/internal/cursor/backend-client.test.ts +591 -0
  124. package/src/internal/cursor/credentials.test.ts +351 -0
  125. package/src/internal/cursor/github-credentials.test.ts +136 -0
  126. package/src/internal/cursor-account-mcp-auth.test.ts +310 -0
  127. package/src/internal/cursor-account.integration.test.ts +441 -0
  128. package/src/internal/cursor-event-relay.test.ts +746 -0
  129. package/src/internal/cursor-github-credentials.integration.test.ts +271 -0
  130. package/src/internal/cursor-slack-relay.test.ts +525 -0
  131. package/src/internal/deploy-source.test.ts +111 -0
  132. package/src/internal/discovery.artifact-tool.test.ts +136 -0
  133. package/src/internal/discovery.artifacts.test.ts +119 -0
  134. package/src/internal/discovery.builtin-tools.test.ts +94 -0
  135. package/src/internal/discovery.concurrency.test.ts +60 -0
  136. package/src/internal/discovery.cursor-account.test.ts +133 -0
  137. package/src/internal/discovery.cwd.test.ts +83 -0
  138. package/src/internal/discovery.hosting.test.ts +80 -0
  139. package/src/internal/discovery.identity.test.ts +44 -0
  140. package/src/internal/discovery.ts +102 -1
  141. package/src/internal/distribution.ts +1 -0
  142. package/src/internal/docs-site.test.ts +66 -0
  143. package/src/internal/duration.test.ts +29 -0
  144. package/src/internal/eval-judge-model.test.ts +187 -0
  145. package/src/internal/eval-run-store.cancel.test.ts +142 -0
  146. package/src/internal/eval-run-store.storage.test.ts +211 -0
  147. package/src/internal/eval-runner.http.test.ts +403 -0
  148. package/src/internal/eval-runner.run.test.ts +928 -0
  149. package/src/internal/evals-client.test.ts +307 -0
  150. package/src/internal/event-mapper.test.ts +243 -0
  151. package/src/internal/github-fanout.test.ts +213 -0
  152. package/src/internal/handleAgentServeTrigger.test.ts +185 -0
  153. package/src/internal/host-kv.test.ts +82 -0
  154. package/src/internal/host-platforms.test.ts +126 -0
  155. package/src/internal/http-channel.test.ts +402 -0
  156. package/src/internal/init-project.test.ts +270 -0
  157. package/src/internal/install-cursor-skills.test.ts +262 -0
  158. package/src/internal/local-env.test.ts +120 -0
  159. package/src/internal/log-ring.test.ts +31 -0
  160. package/src/internal/logs-client.test.ts +350 -0
  161. package/src/internal/mcp-endpoint.test.ts +436 -0
  162. package/src/internal/mcp-host.test.ts +298 -0
  163. package/src/internal/mcp-oauth.test.ts +148 -0
  164. package/src/internal/net.test.ts +17 -0
  165. package/src/internal/peer-connections.test.ts +128 -0
  166. package/src/internal/peer-mcp.integration.test.ts +289 -0
  167. package/src/internal/playground/toolchain.test.ts +53 -0
  168. package/src/internal/playground-cli.test.ts +187 -0
  169. package/src/internal/playground-proxy.test.ts +376 -0
  170. package/src/internal/prompt-context.integration.test.ts +232 -0
  171. package/src/internal/prompt-context.test.ts +127 -0
  172. package/src/internal/reminder-runner.test.ts +432 -0
  173. package/src/internal/reminder-runner.ts +1 -0
  174. package/src/internal/reminder-store.test.ts +53 -0
  175. package/src/internal/request-headers.test.ts +27 -0
  176. package/src/internal/resolve-prod-target.test.ts +787 -0
  177. package/src/internal/resolved-connections.test.ts +295 -0
  178. package/src/internal/router.test.ts +57 -0
  179. package/src/internal/schedule-runner.ts +1 -0
  180. package/src/internal/sdk-runner.test.ts +290 -0
  181. package/src/internal/server.artifacts.test.ts +334 -0
  182. package/src/internal/server.ts +45 -0
  183. package/src/internal/session-engine.artifacts.test.ts +293 -0
  184. package/src/internal/session-engine.coalesce.test.ts +169 -0
  185. package/src/internal/session-engine.concurrency.test.ts +250 -0
  186. package/src/internal/session-engine.host-oauth-mcp.test.ts +110 -0
  187. package/src/internal/session-engine.interrupt.test.ts +577 -0
  188. package/src/internal/session-engine.storage.test.ts +547 -0
  189. package/src/internal/session-engine.ts +129 -1
  190. package/src/internal/session-urls.test.ts +28 -0
  191. package/src/internal/sessions-client.test.ts +518 -0
  192. package/src/internal/storage-coordinator.test.ts +517 -0
  193. package/src/internal/storage-coordinator.ts +62 -0
  194. package/src/internal/tool-call.test.ts +458 -0
  195. package/src/internal/tool-result.test.ts +52 -0
  196. package/src/internal/trajectory.approvals.test.ts +83 -0
  197. package/src/internal/trajectory.subagents.test.ts +198 -0
  198. package/src/internal/turn-governor.test.ts +137 -0
  199. package/src/internal/update-check.test.ts +485 -0
  200. package/src/internal/workspace.test.ts +207 -0
  201. package/src/storage-backends/cursor-hosted.test.ts +121 -0
  202. package/src/storage.ts +6 -0
  203. package/src/types.ts +152 -2
  204. package/dist/docs/assets/chunks/@localSearchIndexroot.CL2Y0Zmh.js +0 -1
  205. package/dist/playground/assets/index-CidizGZv.css +0 -1
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Durable artifact rows behind `ctx.artifacts` ({@link ArtifactsApi}).
3
+ *
4
+ * Mirror of host-kv: when `defineStorage` is authored with `get` + `list`,
5
+ * rows live in that sink under `storageKeys.artifact`; otherwise one
6
+ * JSON file per row under `--state-root/artifacts`.
7
+ */
8
+
9
+ import { createHash, randomUUID } from "node:crypto";
10
+ import {
11
+ mkdir,
12
+ readdir,
13
+ readFile,
14
+ rename,
15
+ rm,
16
+ writeFile,
17
+ } from "node:fs/promises";
18
+ import { join } from "node:path";
19
+ import { resolveArtifactsMax } from "../artifacts.js";
20
+ import type {
21
+ ArtifactListFilter,
22
+ ArtifactRecord,
23
+ ArtifactsConfig,
24
+ ArtifactTagInput,
25
+ JsonValue,
26
+ } from "../types.js";
27
+ import type { StorageCoordinator } from "./storage-coordinator.js";
28
+
29
+ // TODO(agent-stores): the KV backing store is migrating to Agent Stores.
30
+ // Swap only this adapter to the Agent Stores API when that lands; the
31
+ // engine, tool, HTTP, and playground layers must not change.
32
+ interface ArtifactRowStore {
33
+ get(id: string): Promise<ArtifactRecord | undefined>;
34
+ put(record: ArtifactRecord): Promise<void>;
35
+ delete(id: string): Promise<void>;
36
+ list(): Promise<ArtifactRecord[]>;
37
+ }
38
+
39
+ export interface CreateArtifactsStoreOptions {
40
+ stateRoot: string;
41
+ storage?: StorageCoordinator;
42
+ /** `agent/artifacts.ts` config, when authored. */
43
+ config?: ArtifactsConfig;
44
+ }
45
+
46
+ /** Kind used by freeform tags when no kinds are declared. */
47
+ export const ARTIFACT_DEFAULT_KIND = "artifact";
48
+
49
+ /**
50
+ * Upsert + list semantics over one {@link ArtifactRowStore}: kind/schema
51
+ * validation, deterministic ids for keyed tags, and `max` eviction. The
52
+ * engine wraps this into the per-context {@link ArtifactsApi} facades and
53
+ * owns the `artifact.tagged` event append.
54
+ */
55
+ export class ArtifactsStore {
56
+ private readonly rows: ArtifactRowStore;
57
+ private readonly config: ArtifactsConfig | undefined;
58
+ private readonly max: number;
59
+ /** Serializes tags so read-modify-write upserts and eviction don't race. */
60
+ private tagChain: Promise<unknown> = Promise.resolve();
61
+
62
+ constructor(options: CreateArtifactsStoreOptions) {
63
+ this.rows =
64
+ options.storage?.supportsArtifacts === true
65
+ ? createCoordinatorRowStore(options.storage)
66
+ : createFilesystemRowStore(options.stateRoot);
67
+ this.config = options.config;
68
+ this.max = resolveArtifactsMax(options.config);
69
+ }
70
+
71
+ tag(input: ArtifactTagInput): Promise<ArtifactRecord> {
72
+ const result = this.tagChain.then(() => this.tagSerialized(input));
73
+ this.tagChain = result.catch(() => {});
74
+ return result;
75
+ }
76
+
77
+ private async tagSerialized(
78
+ input: ArtifactTagInput
79
+ ): Promise<ArtifactRecord> {
80
+ const { kind, data } = this.resolveKindAndData(input);
81
+ const id =
82
+ input.key === undefined
83
+ ? `art_${randomUUID()}`
84
+ : artifactIdForKey(input.key);
85
+ const existing = await this.rows.get(id);
86
+ const now = new Date().toISOString();
87
+ const record: ArtifactRecord = {
88
+ id,
89
+ kind,
90
+ key: input.key,
91
+ title: input.title,
92
+ data,
93
+ sessionId: input.sessionId,
94
+ turnId: input.turnId,
95
+ source: input.source ?? "host",
96
+ createdAt: existing?.createdAt ?? now,
97
+ updatedAt: now,
98
+ };
99
+ await this.rows.put(record);
100
+ if (existing === undefined) {
101
+ await this.evictPastCap();
102
+ }
103
+ return record;
104
+ }
105
+
106
+ async list(filter?: ArtifactListFilter): Promise<ArtifactRecord[]> {
107
+ const records = await this.rows.list();
108
+ return records
109
+ .filter(
110
+ (record) =>
111
+ (filter?.kind === undefined || record.kind === filter.kind) &&
112
+ (filter?.sessionId === undefined ||
113
+ record.sessionId === filter.sessionId)
114
+ )
115
+ .sort(byNewestUpdated);
116
+ }
117
+
118
+ /**
119
+ * Resolve the effective kind (freeform when no kinds are declared) and
120
+ * the payload to persist. Like tool input schemas
121
+ * (`validateToolInput`), zod schemas validate *and* transform: the
122
+ * parsed output — defaults and coercions applied — is what gets stored.
123
+ */
124
+ private resolveKindAndData(input: ArtifactTagInput): {
125
+ kind: string;
126
+ data: JsonValue;
127
+ } {
128
+ const kind = input.kind ?? ARTIFACT_DEFAULT_KIND;
129
+ if (kind.trim() === "") {
130
+ throw new Error("artifacts.tag: kind must be a non-empty string");
131
+ }
132
+ const kinds = this.config?.kinds;
133
+ if (kinds === undefined || Object.keys(kinds).length === 0) {
134
+ return { kind, data: input.data };
135
+ }
136
+ const kindConfig = kinds[kind];
137
+ if (kindConfig === undefined) {
138
+ throw new Error(
139
+ `artifacts.tag: unknown kind ${JSON.stringify(kind)} (declared: ${Object.keys(kinds).join(", ")})`
140
+ );
141
+ }
142
+ if (kindConfig.schema !== undefined) {
143
+ const parsed = kindConfig.schema.safeParse(input.data);
144
+ if (!parsed.success) {
145
+ throw new Error(
146
+ `artifacts.tag: data does not match the ${JSON.stringify(kind)} schema: ${parsed.error.message}`
147
+ );
148
+ }
149
+ return { kind, data: parsed.data as JsonValue };
150
+ }
151
+ return { kind, data: input.data };
152
+ }
153
+
154
+ private async evictPastCap(): Promise<void> {
155
+ const records = await this.rows.list();
156
+ if (records.length <= this.max) {
157
+ return;
158
+ }
159
+ const oldestFirst = records.sort((a, b) => byNewestUpdated(b, a));
160
+ for (const record of oldestFirst.slice(0, records.length - this.max)) {
161
+ await this.rows.delete(record.id);
162
+ }
163
+ }
164
+ }
165
+
166
+ /** Deterministic id for a keyed tag (same key ⇒ same id ⇒ upsert). */
167
+ export function artifactIdForKey(key: string): string {
168
+ if (key.trim() === "") {
169
+ throw new Error("artifacts.tag: key must be a non-empty string");
170
+ }
171
+ return `art_${createHash("sha256").update(key, "utf8").digest("hex").slice(0, 32)}`;
172
+ }
173
+
174
+ function byNewestUpdated(a: ArtifactRecord, b: ArtifactRecord): number {
175
+ if (a.updatedAt !== b.updatedAt) {
176
+ return a.updatedAt < b.updatedAt ? 1 : -1;
177
+ }
178
+ // Tie-break so eviction and listing stay deterministic.
179
+ if (a.createdAt !== b.createdAt) {
180
+ return a.createdAt < b.createdAt ? 1 : -1;
181
+ }
182
+ return a.id < b.id ? 1 : a.id > b.id ? -1 : 0;
183
+ }
184
+
185
+ function isArtifactRecord(
186
+ value: JsonValue | undefined
187
+ ): value is ArtifactRecord & JsonValue {
188
+ if (
189
+ typeof value !== "object" ||
190
+ value === null ||
191
+ Array.isArray(value) ||
192
+ typeof value.id !== "string" ||
193
+ typeof value.kind !== "string" ||
194
+ (value.source !== "host" && value.source !== "model") ||
195
+ typeof value.createdAt !== "string" ||
196
+ typeof value.updatedAt !== "string" ||
197
+ !("data" in value)
198
+ ) {
199
+ return false;
200
+ }
201
+ for (const field of ["key", "title", "sessionId", "turnId"] as const) {
202
+ if (value[field] !== undefined && typeof value[field] !== "string") {
203
+ return false;
204
+ }
205
+ }
206
+ return true;
207
+ }
208
+
209
+ function createCoordinatorRowStore(
210
+ storage: StorageCoordinator
211
+ ): ArtifactRowStore {
212
+ return {
213
+ async get(id): Promise<ArtifactRecord | undefined> {
214
+ const value = await storage.artifactGet(id);
215
+ return isArtifactRecord(value) ? value : undefined;
216
+ },
217
+ async put(record): Promise<void> {
218
+ await storage.artifactPut(record.id, record as unknown as JsonValue);
219
+ },
220
+ async delete(id): Promise<void> {
221
+ await storage.artifactDelete(id);
222
+ },
223
+ async list(): Promise<ArtifactRecord[]> {
224
+ const values = await storage.artifactList();
225
+ return values.filter(isArtifactRecord);
226
+ },
227
+ };
228
+ }
229
+
230
+ function createFilesystemRowStore(stateRoot: string): ArtifactRowStore {
231
+ const dir = join(stateRoot, "artifacts");
232
+ const filePath = (id: string): string =>
233
+ join(dir, `${encodeURIComponent(id)}.json`);
234
+
235
+ // Missing and corrupt rows both read as absent — a truncated/garbled
236
+ // file must not poison list() (and with it every insert's eviction).
237
+ const readRecord = async (
238
+ path: string
239
+ ): Promise<ArtifactRecord | undefined> => {
240
+ try {
241
+ const value = JSON.parse(await readFile(path, "utf8")) as JsonValue;
242
+ return isArtifactRecord(value) ? value : undefined;
243
+ } catch (error) {
244
+ if (
245
+ (error as NodeJS.ErrnoException).code === "ENOENT" ||
246
+ error instanceof SyntaxError
247
+ ) {
248
+ return undefined;
249
+ }
250
+ throw error;
251
+ }
252
+ };
253
+
254
+ return {
255
+ get(id): Promise<ArtifactRecord | undefined> {
256
+ return readRecord(filePath(id));
257
+ },
258
+ async put(record): Promise<void> {
259
+ await mkdir(dir, { recursive: true });
260
+ const tmp = join(dir, `.tmp-${randomUUID()}`);
261
+ await writeFile(tmp, JSON.stringify(record), "utf8");
262
+ await rename(tmp, filePath(record.id));
263
+ },
264
+ async delete(id): Promise<void> {
265
+ await rm(filePath(id), { force: true });
266
+ },
267
+ async list(): Promise<ArtifactRecord[]> {
268
+ let names: string[];
269
+ try {
270
+ names = await readdir(dir);
271
+ } catch (error) {
272
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
273
+ return [];
274
+ }
275
+ throw error;
276
+ }
277
+ const records: ArtifactRecord[] = [];
278
+ for (const name of names.sort()) {
279
+ if (name.startsWith(".") || !name.endsWith(".json")) {
280
+ continue;
281
+ }
282
+ const record = await readRecord(join(dir, name));
283
+ if (record !== undefined) {
284
+ records.push(record);
285
+ }
286
+ }
287
+ return records;
288
+ },
289
+ };
290
+ }
@@ -0,0 +1,31 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { describe, expect, it } from "vitest";
3
+ import { AUTHORING_ENTRY_FILES } from "./distribution.js";
4
+
5
+ describe("AUTHORING_ENTRY_FILES", () => {
6
+ it("mirrors the package exports map (the runtime alias hooks derive their targets from it)", () => {
7
+ const pkg = JSON.parse(
8
+ readFileSync(new URL("../../package.json", import.meta.url), "utf8")
9
+ ) as {
10
+ exports: Record<string, string | { import?: string }>;
11
+ };
12
+ const fromExports = Object.entries(pkg.exports)
13
+ .filter(
14
+ ([exportPath, conditions]) =>
15
+ typeof conditions !== "string" && exportPath !== "./package.json"
16
+ )
17
+ .map(([exportPath, conditions]) => {
18
+ const subpath =
19
+ exportPath === "." ? "" : exportPath.replace(/^\.\//, "");
20
+ const importTarget = (conditions as { import?: string }).import;
21
+ expect(
22
+ importTarget,
23
+ `exports["${exportPath}"] must have an import condition`
24
+ ).toBeDefined();
25
+ return [subpath, importTarget?.replace(/^\.\/dist\//, "")];
26
+ });
27
+ expect(AUTHORING_ENTRY_FILES.map((entry) => [...entry])).toEqual(
28
+ fromExports
29
+ );
30
+ });
31
+ });
@@ -0,0 +1,247 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+ import { z } from "zod";
6
+ import { defineArtifacts } from "../../artifacts.js";
7
+ import { httpChannel } from "../../channels.js";
8
+ import type { AgentProject, JsonSchemaObject } from "../../types.js";
9
+ import { toolInputJsonSchema } from "../json-schema.js";
10
+ import { SessionEngine, ToolInputError } from "../session-engine.js";
11
+ import { tagArtifactTool } from "./artifacts.js";
12
+ import { artifactBuiltinTool, TAG_ARTIFACT_TOOL_NAME } from "./index.js";
13
+
14
+ const KINDS = defineArtifacts({
15
+ kinds: {
16
+ "reviewed-pr": {
17
+ description: "A pull request this agent reviewed.",
18
+ schema: z.object({ url: z.string(), verdict: z.string() }),
19
+ },
20
+ report: { description: "A generated report." },
21
+ },
22
+ agentTool: true,
23
+ });
24
+
25
+ describe("tagArtifactTool schema", () => {
26
+ it("builds a discriminated union over the declared kinds", () => {
27
+ const definition = tagArtifactTool(KINDS);
28
+ const jsonSchema = toolInputJsonSchema(definition.inputSchema);
29
+ const variants = (jsonSchema.anyOf ?? jsonSchema.oneOf) as Array<{
30
+ properties: Record<string, JsonSchemaObject>;
31
+ required?: string[];
32
+ }>;
33
+ expect(variants).toHaveLength(2);
34
+ expect(variants.map((v) => v.properties.kind?.const)).toEqual([
35
+ "reviewed-pr",
36
+ "report",
37
+ ]);
38
+ // The kind's zod schema shapes `data`; kinds without one pass through.
39
+ expect(variants[0]?.properties.data?.properties).toMatchObject({
40
+ url: { type: "string" },
41
+ verdict: { type: "string" },
42
+ });
43
+ expect(variants[1]?.properties.data?.additionalProperties).toBe(true);
44
+ for (const variant of variants) {
45
+ expect(variant.required).toEqual(
46
+ expect.arrayContaining(["kind", "data"])
47
+ );
48
+ expect(variant.required).not.toContain("title");
49
+ expect(variant.required).not.toContain("key");
50
+ }
51
+ });
52
+
53
+ it("keeps the author's data schema description", () => {
54
+ const definition = tagArtifactTool({
55
+ kinds: {
56
+ note: {
57
+ description: "A note.",
58
+ schema: z.object({ text: z.string() }).describe("Author-described."),
59
+ },
60
+ },
61
+ agentTool: true,
62
+ });
63
+ const jsonSchema = toolInputJsonSchema(definition.inputSchema);
64
+ const variants = (jsonSchema.anyOf ?? jsonSchema.oneOf) as Array<{
65
+ properties: Record<string, JsonSchemaObject>;
66
+ }>;
67
+ expect(variants[0]?.properties.data?.description).toBe("Author-described.");
68
+ });
69
+
70
+ it("rejects payloads that miss the kind schema", () => {
71
+ const definition = tagArtifactTool(KINDS);
72
+ const schema = definition.inputSchema as z.ZodTypeAny;
73
+ expect(
74
+ schema.safeParse({ kind: "reviewed-pr", data: { url: 1 } }).success
75
+ ).toBe(false);
76
+ expect(
77
+ schema.safeParse({
78
+ kind: "reviewed-pr",
79
+ data: { url: "https://x", verdict: "approved" },
80
+ }).success
81
+ ).toBe(true);
82
+ expect(schema.safeParse({ kind: "unknown", data: {} }).success).toBe(false);
83
+ });
84
+
85
+ it("rejects blank keys at the schema boundary and trims valid ones", () => {
86
+ const definition = tagArtifactTool(KINDS);
87
+ const schema = definition.inputSchema as z.ZodTypeAny;
88
+ expect(
89
+ schema.safeParse({ kind: "report", data: {}, key: "" }).success
90
+ ).toBe(false);
91
+ expect(
92
+ schema.safeParse({ kind: "report", data: {}, key: " " }).success
93
+ ).toBe(false);
94
+ const parsed = schema.safeParse({
95
+ kind: "report",
96
+ data: {},
97
+ key: " pr-1 ",
98
+ });
99
+ expect(parsed.success).toBe(true);
100
+ expect((parsed as { data: { key: string } }).data.key).toBe("pr-1");
101
+ });
102
+
103
+ it("prompts with the kind descriptions and key upsert semantics", () => {
104
+ const definition = tagArtifactTool(KINDS);
105
+ expect(definition.description).toContain(
106
+ "- reviewed-pr: A pull request this agent reviewed."
107
+ );
108
+ expect(definition.description).toContain("- report: A generated report.");
109
+ expect(definition.description).toContain("upsert");
110
+ expect(definition.description).toContain("stable key");
111
+ });
112
+
113
+ it("requires at least one declared kind", () => {
114
+ expect(() => tagArtifactTool({ kinds: {} })).toThrow(/at least one/);
115
+ });
116
+ });
117
+
118
+ describe("tagArtifactTool execute", () => {
119
+ const cleanups: Array<() => Promise<void>> = [];
120
+
121
+ afterEach(async () => {
122
+ while (cleanups.length > 0) {
123
+ await cleanups.pop()?.();
124
+ }
125
+ });
126
+
127
+ async function makeEngine(
128
+ logger?: (line: string) => void
129
+ ): Promise<SessionEngine> {
130
+ const stateRoot = await mkdtemp(join(tmpdir(), "agent-serve-tagtool-"));
131
+ cleanups.push(() => rm(stateRoot, { recursive: true, force: true }));
132
+ const tool = artifactBuiltinTool(KINDS);
133
+ if (tool === undefined) {
134
+ throw new Error("expected tag_artifact to be enabled");
135
+ }
136
+ const project: AgentProject = {
137
+ rootDir: stateRoot,
138
+ agentDir: join(stateRoot, "agent"),
139
+ name: "tag-artifact-test",
140
+ agent: {
141
+ name: "tag-artifact-test",
142
+ runtime: "local",
143
+ instructions: "Test agent.",
144
+ tools: [tool],
145
+ skills: [],
146
+ connections: [],
147
+ subagents: [],
148
+ seedFiles: [],
149
+ },
150
+ httpChannel: httpChannel(),
151
+ channels: [],
152
+ schedules: [],
153
+ hooks: [],
154
+ abs: [],
155
+ artifacts: KINDS,
156
+ diagnostics: [],
157
+ };
158
+ const engine = new SessionEngine({
159
+ project,
160
+ stateRoot,
161
+ logger,
162
+ runner: {
163
+ runTurn: () => {
164
+ throw new Error("no model turns in this test");
165
+ },
166
+ },
167
+ });
168
+ cleanups.push(() => engine.close());
169
+ return engine;
170
+ }
171
+
172
+ it("tags through ctx.artifacts with source model and confirms", async () => {
173
+ const engine = await makeEngine();
174
+ const outcome = await engine.callTool(TAG_ARTIFACT_TOOL_NAME, {
175
+ kind: "reviewed-pr",
176
+ data: { url: "https://github.com/org/repo/pull/1", verdict: "lgtm" },
177
+ title: "PR #1",
178
+ key: "pr-1",
179
+ });
180
+ expect(outcome.isError).toBe(false);
181
+ const rows = await engine.artifacts.list();
182
+ expect(rows).toHaveLength(1);
183
+ expect(rows[0]).toMatchObject({
184
+ kind: "reviewed-pr",
185
+ key: "pr-1",
186
+ title: "PR #1",
187
+ data: { url: "https://github.com/org/repo/pull/1", verdict: "lgtm" },
188
+ source: "model",
189
+ });
190
+ expect(outcome.result).toEqual({
191
+ tagged: true,
192
+ id: rows[0]?.id,
193
+ kind: "reviewed-pr",
194
+ key: "pr-1",
195
+ });
196
+ });
197
+
198
+ it("logs the tag without the data payload", async () => {
199
+ const lines: string[] = [];
200
+ const engine = await makeEngine((line) => lines.push(line));
201
+ await engine.callTool(TAG_ARTIFACT_TOOL_NAME, {
202
+ kind: "report",
203
+ data: { secret: "do-not-log" },
204
+ key: "r-1",
205
+ });
206
+ const [record] = await engine.artifacts.list();
207
+ const logged = lines.find((line) =>
208
+ line.includes("[session] artifact tagged")
209
+ );
210
+ expect(logged).toBe(
211
+ `[session] artifact tagged kind=report id=${record?.id} source=model session=- key=r-1`
212
+ );
213
+ expect(lines.join("\n")).not.toContain("do-not-log");
214
+ });
215
+
216
+ it("rejects undeclared kinds and schema-invalid payloads at the boundary", async () => {
217
+ const engine = await makeEngine();
218
+ await expect(
219
+ engine.callTool(TAG_ARTIFACT_TOOL_NAME, { kind: "unknown", data: {} })
220
+ ).rejects.toThrow(ToolInputError);
221
+ await expect(
222
+ engine.callTool(TAG_ARTIFACT_TOOL_NAME, {
223
+ kind: "reviewed-pr",
224
+ data: { url: 1 },
225
+ })
226
+ ).rejects.toThrow(ToolInputError);
227
+ });
228
+ });
229
+
230
+ describe("artifactBuiltinTool", () => {
231
+ it("is enabled only by agentTool: true with kinds", () => {
232
+ expect(artifactBuiltinTool(undefined)).toBeUndefined();
233
+ expect(
234
+ artifactBuiltinTool({
235
+ kinds: { report: { description: "A report." } },
236
+ })
237
+ ).toBeUndefined();
238
+ expect(artifactBuiltinTool({ agentTool: true, kinds: {} })).toBeUndefined();
239
+
240
+ const tool = artifactBuiltinTool(KINDS);
241
+ expect(tool).toMatchObject({
242
+ name: TAG_ARTIFACT_TOOL_NAME,
243
+ execution: "server",
244
+ needsApproval: false,
245
+ });
246
+ });
247
+ });
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The `tag_artifact` built-in tool, enabled by
3
+ * `defineArtifacts({ agentTool: true })` rather than
4
+ * `AgentConfig.builtinTools`: its input schema and description are
5
+ * generated from the declared kinds registry.
6
+ */
7
+
8
+ import { z } from "zod";
9
+ import { defineTool } from "../../tools.js";
10
+ import type { ArtifactsConfig, ToolDefinition } from "../../types.js";
11
+
12
+ export const TAG_ARTIFACT_TOOL_NAME = "tag_artifact";
13
+
14
+ function kindVariant(name: string, schema: z.ZodType | undefined) {
15
+ return z.object({
16
+ kind: z.literal(name),
17
+ data:
18
+ schema ??
19
+ z
20
+ .object({})
21
+ .passthrough()
22
+ .describe("Freeform JSON object payload for this kind."),
23
+ title: z
24
+ .string()
25
+ .optional()
26
+ .describe("Short human-readable label shown in artifact lists."),
27
+ key: z
28
+ .string()
29
+ .trim()
30
+ .min(1)
31
+ .optional()
32
+ .describe(
33
+ "Stable upsert key: tagging the same key again updates the existing record instead of creating a new one."
34
+ ),
35
+ });
36
+ }
37
+
38
+ export function tagArtifactTool(config: ArtifactsConfig): ToolDefinition {
39
+ const kinds = Object.entries(config.kinds ?? {});
40
+ const [first, ...rest] = kinds.map(([name, kind]) =>
41
+ kindVariant(name, kind.schema)
42
+ );
43
+ if (first === undefined) {
44
+ // Discovery already errors on agentTool without kinds.
45
+ throw new Error(
46
+ "tag_artifact requires at least one declared artifact kind"
47
+ );
48
+ }
49
+ const description = [
50
+ "Tag a durable artifact: a record of a notable output you produced, kept across sessions and listed for the people supervising this agent. Tag one whenever you finish something worth surfacing durably.",
51
+ "Kinds:",
52
+ ...kinds.map(([name, kind]) => `- ${name}: ${kind.description}`),
53
+ "Same `key` upserts: tagging it again updates that record — use a stable key when re-tagging the same underlying thing (e.g. the same PR URL).",
54
+ ].join("\n");
55
+ return defineTool({
56
+ description,
57
+ inputSchema: z.discriminatedUnion("kind", [first, ...rest]),
58
+ async execute(input, ctx) {
59
+ const record = await ctx.artifacts.tag({
60
+ kind: input.kind,
61
+ data: input.data,
62
+ title: input.title,
63
+ key: input.key,
64
+ source: "model",
65
+ });
66
+ return {
67
+ tagged: true,
68
+ id: record.id,
69
+ kind: record.kind,
70
+ key: record.key,
71
+ };
72
+ },
73
+ });
74
+ }
@@ -6,13 +6,17 @@
6
6
  */
7
7
 
8
8
  import type {
9
+ ArtifactsConfig,
9
10
  BuiltinToolsConfig,
10
11
  DiscoveredTool,
11
12
  ToolDefinition,
12
13
  } from "../../types.js";
13
14
  import { toolInputJsonSchema } from "../json-schema.js";
15
+ import { TAG_ARTIFACT_TOOL_NAME, tagArtifactTool } from "./artifacts.js";
14
16
  import { reminderTools } from "./reminders.js";
15
17
 
18
+ export { TAG_ARTIFACT_TOOL_NAME } from "./artifacts.js";
19
+
16
20
  /** Resolve the enabled built-in tools, in stable catalog order. */
17
21
  export function builtinTools(
18
22
  config: BuiltinToolsConfig | undefined
@@ -24,6 +28,23 @@ export function builtinTools(
24
28
  return tools;
25
29
  }
26
30
 
31
+ /**
32
+ * The `tag_artifact` built-in, enabled by `defineArtifacts({ agentTool:
33
+ * true })` (not `AgentConfig.builtinTools` — the tool is generated from
34
+ * the kinds registry). Undefined without kinds: discovery already errors.
35
+ */
36
+ export function artifactBuiltinTool(
37
+ config: ArtifactsConfig | undefined
38
+ ): DiscoveredTool | undefined {
39
+ if (
40
+ config?.agentTool !== true ||
41
+ Object.keys(config.kinds ?? {}).length === 0
42
+ ) {
43
+ return undefined;
44
+ }
45
+ return toDiscovered({ [TAG_ARTIFACT_TOOL_NAME]: tagArtifactTool(config) })[0];
46
+ }
47
+
27
48
  function toDiscovered(
28
49
  byName: Record<string, ToolDefinition>
29
50
  ): DiscoveredTool[] {