@anteros/core 0.0.41 → 0.0.44

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 (133) hide show
  1. package/database/audit.ts +9 -59
  2. package/database/collection.ts +3 -1
  3. package/database/deleteLog.ts +67 -3
  4. package/database/file.ts +4 -3
  5. package/database/mongodbadapter.ts +103 -42
  6. package/database/replication.ts +724 -58
  7. package/database/ttl.ts +68 -0
  8. package/database/workflow.ts +466 -240
  9. package/index.ts +6 -0
  10. package/lib/agent.ts +1567 -0
  11. package/lib/agentMemory.ts +689 -0
  12. package/lib/agents.ts +180 -0
  13. package/lib/ai.ts +0 -0
  14. package/lib/api.ts +246 -0
  15. package/lib/attachment.ts +351 -0
  16. package/lib/auditResult.ts +13 -0
  17. package/lib/define.ts +43 -1
  18. package/lib/endpoints.ts +80 -0
  19. package/lib/files.ts +2 -1
  20. package/lib/jsonSchema.ts +183 -0
  21. package/lib/lifecycle.ts +7 -1
  22. package/lib/load.ts +17 -0
  23. package/lib/mcp.ts +3 -2
  24. package/lib/mcpClient.ts +128 -0
  25. package/lib/messages.ts +299 -0
  26. package/lib/providers.ts +970 -0
  27. package/lib/routes.ts +14 -1
  28. package/lib/scriptWorker.ts +4 -1
  29. package/lib/scripts.ts +6 -1
  30. package/lib/skills.ts +481 -0
  31. package/lib/sockets.ts +3 -1
  32. package/lib/workflow.ts +209 -37
  33. package/lib/zip.ts +109 -0
  34. package/package.json +1 -1
  35. package/server/access.ts +96 -0
  36. package/server/agents.ts +472 -0
  37. package/server/api.ts +80 -65
  38. package/server/boot.ts +11 -0
  39. package/server/hono.ts +8 -3
  40. package/server/mcp.ts +17 -63
  41. package/server/routes.ts +19 -24
  42. package/tests/agent-mcp.test.ts +116 -0
  43. package/tests/agent-memory.test.ts +617 -0
  44. package/tests/agent-provider.test.ts +288 -0
  45. package/tests/agent-runtime.test.ts +369 -0
  46. package/tests/agent-skills.test.ts +177 -0
  47. package/tests/agent-summarize.test.ts +124 -0
  48. package/tests/agent-working-memory.test.ts +404 -0
  49. package/tests/agent.test.ts +1241 -0
  50. package/tests/agents-api.test.ts +593 -0
  51. package/tests/api-private-fields.test.ts +196 -0
  52. package/tests/api.test.ts +51 -0
  53. package/tests/attachment.test.ts +241 -0
  54. package/tests/audit-operations.test.ts +1 -1
  55. package/tests/audit-redact.test.ts +29 -3
  56. package/tests/audit-run-action.test.ts +1 -1
  57. package/tests/endpoints.test.ts +80 -0
  58. package/tests/file-collection-replication.test.ts +2 -0
  59. package/tests/fixtures/agent-tenant/agents/support.agent.ts +9 -0
  60. package/tests/fixtures/agent-tenant/agents/weather.agent.ts +21 -0
  61. package/tests/fixtures/agents-api/agents/assistant.agent.ts +46 -0
  62. package/tests/fixtures/agents-api/agents/plain.agent.ts +23 -0
  63. package/tests/fixtures/agents-api/agents/private.agent.ts +15 -0
  64. package/tests/fixtures/agents-api/collections/notes.model.ts +26 -0
  65. package/tests/fixtures/agents-api/routes/probe.route.ts +21 -0
  66. package/tests/fixtures/agents-api/services/probe.service.ts +19 -0
  67. package/tests/fixtures/attachment/data.csv +2 -0
  68. package/tests/fixtures/fake-provider.ts +128 -0
  69. package/tests/fixtures/repl-only/app.ts +3 -0
  70. package/tests/fixtures/replication-meta/agents/chat.agent.ts +16 -0
  71. package/tests/fixtures/replication-meta/collections/counters.model.ts +13 -0
  72. package/tests/fixtures/replication-meta/collections/orders.model.ts +9 -0
  73. package/tests/fixtures/replication-meta/workflows/run.workflow.ts +8 -0
  74. package/tests/fixtures/skills-tenant/agents/docs.agent.ts +23 -0
  75. package/tests/fixtures/skills-tenant/agents/incomplete.agent.ts +14 -0
  76. package/tests/fixtures/skills-tenant/broken/bad-name.md +6 -0
  77. package/tests/fixtures/skills-tenant/broken/dup-a.md +6 -0
  78. package/tests/fixtures/skills-tenant/broken/dup-b.md +6 -0
  79. package/tests/fixtures/skills-tenant/broken/empty-body.md +4 -0
  80. package/tests/fixtures/skills-tenant/broken/long-compat.md +7 -0
  81. package/tests/fixtures/skills-tenant/broken/long-description.md +6 -0
  82. package/tests/fixtures/skills-tenant/broken/mismatch/SKILL.md +6 -0
  83. package/tests/fixtures/skills-tenant/broken/no-description.md +5 -0
  84. package/tests/fixtures/skills-tenant/broken/no-name.md +5 -0
  85. package/tests/fixtures/skills-tenant/broken/plain.md +4 -0
  86. package/tests/fixtures/skills-tenant/broken/unknown-key.md +9 -0
  87. package/tests/fixtures/skills-tenant/docs/pdf-forms.md +18 -0
  88. package/tests/fixtures/skills-tenant/guide/ml.md +9 -0
  89. package/tests/fixtures/skills-tenant/guides/pdf/SKILL.md +9 -0
  90. package/tests/fixtures/skills-tenant/runbooks/incident.md +9 -0
  91. package/tests/fixtures/skills-tenant/runbooks/notes.txt +2 -0
  92. package/tests/fixtures/vars-tenant/vars/private.var.ts +4 -1
  93. package/tests/fixtures/workflow-engine/workflows/failing.workflow.ts +34 -0
  94. package/tests/fixtures/workflow-engine/workflows/flaky.workflow.ts +23 -0
  95. package/tests/fixtures/workflow-engine/workflows/hang.workflow.ts +17 -0
  96. package/tests/fixtures/workflow-engine/workflows/simple.workflow.ts +19 -0
  97. package/tests/fixtures/workflow-engine/workflows/slow.workflow.ts +23 -0
  98. package/tests/fixtures/workflow-engine/workflows/versioned.workflow.ts +19 -0
  99. package/tests/fixtures/workflow-tenant/workflows/broken.workflow.ts +3 -0
  100. package/tests/fixtures/workflow-tenant/workflows/disabled.workflow.ts +8 -0
  101. package/tests/fixtures/workflow-tenant/workflows/invoice.workflow.ts +21 -0
  102. package/tests/fixtures/workflow-tenant/workflows/no-id.workflow.ts +7 -0
  103. package/tests/fixtures/workflow-tenant/workflows/order-duplicate.workflow.ts +13 -0
  104. package/tests/fixtures/workflow-tenant/workflows/order.workflow.ts +17 -0
  105. package/tests/hono.test.ts +6 -3
  106. package/tests/locks.test.ts +120 -0
  107. package/tests/mcp.test.ts +11 -11
  108. package/tests/messages.test.ts +258 -0
  109. package/tests/replication-change-stream.test.ts +240 -0
  110. package/tests/replication-control.test.ts +4 -2
  111. package/tests/replication-initial-sync.test.ts +2 -0
  112. package/tests/replication-meta.test.ts +246 -0
  113. package/tests/replication.test.ts +2 -0
  114. package/tests/upload.test.ts +44 -12
  115. package/tests/vars-api.test.ts +1 -1
  116. package/tests/vars-replication.test.ts +5 -2
  117. package/tests/workflow-engine.test.ts +340 -0
  118. package/tests/workflow-retention.test.ts +103 -0
  119. package/tests/workflow.test.ts +186 -0
  120. package/types/agent.d.ts +1067 -0
  121. package/types/config.d.ts +16 -0
  122. package/types/file.d.ts +7 -7
  123. package/types/hook.d.ts +6 -0
  124. package/types/lifecycle.d.ts +6 -0
  125. package/types/mcp.d.ts +11 -1
  126. package/types/replication.d.ts +70 -4
  127. package/types/route.d.ts +7 -1
  128. package/types/scripts.d.ts +6 -0
  129. package/types/service.d.ts +7 -1
  130. package/types/tenant.d.ts +13 -0
  131. package/types/vars.d.ts +2 -2
  132. package/types/websocket.d.ts +6 -0
  133. package/types/workflow.d.ts +50 -3
@@ -0,0 +1,21 @@
1
+ import { define, v } from "../../../../index";
2
+
3
+ /**
4
+ * Loader fixture — `{tenant.dir}/agents/**\/*.agent.ts`.
5
+ * No call is ever made against the provider here: only the loader is exercised.
6
+ */
7
+ export default define.Agent({
8
+ id: "weather",
9
+ name: "Weather Agent",
10
+ description: "Answers weather questions with a tool call",
11
+ instructions: "You are a concise weather assistant.",
12
+ provider: { model: "test-model", protocol: "openai", apiKey: "fixture-key" },
13
+ tools: {
14
+ forecast: define.Tool({
15
+ id: "forecast",
16
+ description: "Current weather for a city",
17
+ inputSchema: v.object({ city: v.string().required() }),
18
+ execute: async ({ city }) => ({ city, celsius: 21 }),
19
+ }),
20
+ },
21
+ });
@@ -0,0 +1,46 @@
1
+ import { define, v, agents } from "../../../../index";
2
+
3
+ /**
4
+ * HTTP-API fixture — exposed through `POST /api/:tenant_id/agents/:agent/:action`.
5
+ *
6
+ * The provider endpoint comes from the environment so the test can point it at
7
+ * its own fake provider (the port is chosen by the OS).
8
+ */
9
+ export default define.Agent({
10
+ id: "assistant",
11
+ name: "Assistant",
12
+ description: "Fixture agent for the HTTP API",
13
+ instructions: "You are a fixture agent.",
14
+ provider: {
15
+ model: "test-model",
16
+ protocol: "openai",
17
+ apiKey: "test-key",
18
+ baseUrl: Bun.env.AGENT_API_TEST_URL ?? "http://127.0.0.1:1",
19
+ options: { retries: 0 },
20
+ },
21
+ maxSteps: 3,
22
+ memory: new agents.memory.InMemory(),
23
+ tools: {
24
+ echo: define.Tool({
25
+ id: "echo",
26
+ description: "Echoes the given text",
27
+ inputSchema: v.object({ text: v.string().required() }),
28
+ execute: async ({ text }) => ({ echoed: text }),
29
+ }),
30
+ },
31
+ api: {
32
+ access: {
33
+ // public metadata — no token needed
34
+ info: true,
35
+ generate: (ctx) => !!ctx.token.value,
36
+ stream: (ctx) => !!ctx.token.value,
37
+ object: (ctx) => !!ctx.token.value,
38
+ history: (ctx) => !!ctx.token.value,
39
+ threads: (ctx) => !!ctx.token.value,
40
+ clear: (ctx) => !!ctx.token.value,
41
+ },
42
+ object: {
43
+ schema: v.object({ city: v.string().required(), celsius: v.number().required() }),
44
+ },
45
+ },
46
+ });
@@ -0,0 +1,23 @@
1
+ import { define } from "../../../../index";
2
+
3
+ /**
4
+ * Public agent — `generate` is allowed without a token. Declares no memory and
5
+ * no `api.object`, to exercise `AGENT_NO_MEMORY` / `AGENT_NO_OBJECT_SCHEMA`.
6
+ */
7
+ export default define.Agent({
8
+ id: "plain",
9
+ description: "A plain agent with no memory and no object schema.",
10
+ instructions: "You are a plain agent.",
11
+ provider: {
12
+ model: "test-model",
13
+ protocol: "openai",
14
+ apiKey: "test-key",
15
+ baseUrl: Bun.env.AGENT_API_TEST_URL ?? "http://127.0.0.1:1",
16
+ options: { retries: 0 },
17
+ },
18
+ api: {
19
+ // public on purpose: `object` also needs a rule so the *missing schema*
20
+ // error can be exercised (otherwise the action is denied first)
21
+ access: { generate: true, object: true },
22
+ },
23
+ });
@@ -0,0 +1,15 @@
1
+ import { define } from "../../../../index";
2
+
3
+ /** Declares no `api` at all — every action must be denied (secure by default). */
4
+ export default define.Agent({
5
+ id: "private",
6
+ description: "Not exposed over HTTP.",
7
+ instructions: "You are a private agent.",
8
+ provider: {
9
+ model: "test-model",
10
+ protocol: "openai",
11
+ apiKey: "test-key",
12
+ baseUrl: Bun.env.AGENT_API_TEST_URL ?? "http://127.0.0.1:1",
13
+ options: { retries: 0 },
14
+ },
15
+ });
@@ -0,0 +1,26 @@
1
+ import { define } from "../../../../index";
2
+
3
+ /**
4
+ * A collection whose hook uses `agents` — proving the LLM registry is injected
5
+ * into the hook context exactly like `rest` and `io`.
6
+ *
7
+ * The observation is published on `globalThis` (the test reads it back): a hook
8
+ * must not stuff arbitrary keys into `meta.data`, the collection schema rejects
9
+ * them (`allowUnknown: false`).
10
+ */
11
+ export default define.Collection({
12
+ slug: "notes",
13
+ fields: [{ name: "title", type: "string" }],
14
+ hooks: {
15
+ beforeOperation: define.Hook(async ({ agents, api, rest, meta }) => {
16
+ if (meta.action !== "insertOne") return;
17
+ (globalThis as any).__agentsHookProbe = {
18
+ ids: agents.ids().sort(),
19
+ bound: agents.get("assistant")?.getRest() === rest,
20
+ // The in-process facade, bound to the same `rest`
21
+ slug: api.collection("notes").getSlug(),
22
+ notes: await api.collection("notes").countDocuments(),
23
+ };
24
+ }),
25
+ },
26
+ });
@@ -0,0 +1,21 @@
1
+ import { define } from "../../../../index";
2
+
3
+ /** A route reading the LLM registry **and the in-process facade** from its context. */
4
+ export default define.Route({
5
+ method: "GET",
6
+ path: "/probe",
7
+ handler: async ({ agents, api, rest }) => Response.json({
8
+ agents: agents.ids().sort(),
9
+ hasAssistant: !!agents.get("assistant"),
10
+ // `api` groups the same calls as `rest` — bound and namespaced
11
+ slug: api.collection("notes").getSlug(),
12
+ notes: await api.collection("notes").countDocuments(),
13
+ sameAsRest: (await api.collection("notes").find()) .length === (await rest.find("notes")).length,
14
+ agentBound: api.agent("assistant")?.getRest() === rest,
15
+ unknownAgent: api.agent("ghost"),
16
+ vars: typeof api.vars.get,
17
+ serviceRun: typeof api.service("probe").run,
18
+ serviceCall: await api.service("probe").run("echo", { from: "route" }),
19
+ fileUrl: api.files.url("invoices", "f1.pdf", { width: 800 }),
20
+ }),
21
+ });
@@ -0,0 +1,19 @@
1
+ import { define } from "../../../../index";
2
+
3
+ /** A service action reading the LLM registry and the facade from its context. */
4
+ export default define.Service({
5
+ name: "probe",
6
+ enabled: true,
7
+ actions: {
8
+ check: async ({ agents, api, rest }) => ({
9
+ agents: agents.ids().sort(),
10
+ hasAssistant: !!agents.get("assistant"),
11
+ bound: agents.get("assistant")?.getRest() === rest,
12
+ tenant: rest.tenant_id,
13
+ // `api.service(name).run` is the grouped `rest.runService`
14
+ nested: await api.service("probe").run("echo", { from: "api" }),
15
+ varType: typeof api.vars.get,
16
+ }),
17
+ echo: async ({ data }) => data,
18
+ },
19
+ });
@@ -0,0 +1,2 @@
1
+ ref,total
2
+ A-1,99
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Fake model providers for the agent tests — one local HTTP endpoint per
3
+ * protocol, recording what the runtime sent.
4
+ *
5
+ * Shared by `agent.test.ts` (runtime) and `agents-api.test.ts` (HTTP surface),
6
+ * so a change in the wire format is fixed in one place.
7
+ */
8
+
9
+ export type Recorded = { path: string; body: any; headers: Record<string, string> };
10
+
11
+ export type Fake = {
12
+ requests: Recorded[];
13
+ url: string;
14
+ hits: () => number;
15
+ stop: () => void;
16
+ };
17
+
18
+ /** A local endpoint that records the requests and answers from `handler`. */
19
+ export function fakeProvider(handler: (body: any, req: Request, hits: number) => Response | Promise<Response>): Fake {
20
+ const requests: Recorded[] = [];
21
+ let hits = 0;
22
+ const server = Bun.serve({
23
+ port: 0,
24
+ hostname: "127.0.0.1",
25
+ fetch: async (req) => {
26
+ const body = await req.json().catch(() => null);
27
+ requests.push({
28
+ path: new URL(req.url).pathname,
29
+ body,
30
+ headers: Object.fromEntries(req.headers.entries()),
31
+ });
32
+ hits += 1;
33
+ return handler(body, req, hits);
34
+ },
35
+ });
36
+ return {
37
+ requests,
38
+ url: `http://127.0.0.1:${server.port}`,
39
+ hits: () => hits,
40
+ stop: () => server.stop(true),
41
+ };
42
+ }
43
+
44
+ /** Typed element access (`noUncheckedIndexedAccess` is on). */
45
+ export function at<T>(list: T[], index = 0): T {
46
+ const item = list[index];
47
+ if (item === undefined) throw new Error(`No element #${index} (length ${list.length})`);
48
+ return item;
49
+ }
50
+
51
+ export function request(provider: Fake, index = 0): Recorded {
52
+ const recorded = provider.requests[index];
53
+ if (!recorded) throw new Error(`No request #${index} was recorded (got ${provider.requests.length})`);
54
+ return recorded;
55
+ }
56
+
57
+ export const USAGE = { prompt_tokens: 11, completion_tokens: 4, total_tokens: 15 };
58
+ export const A_USAGE = { input_tokens: 13, output_tokens: 6 };
59
+
60
+ /** An SSE response from a list of already-serialized events. */
61
+ export function sse(events: string[]): Response {
62
+ return new Response(events.map((event) => `data: ${event}\n\n`).join(""), {
63
+ headers: { "Content-Type": "text/event-stream" },
64
+ });
65
+ }
66
+
67
+ // ─── OpenAI-compatible ───────────────────────────────────────────────────
68
+
69
+ export function openaiText(text: string, finishReason = "stop"): Response {
70
+ return Response.json({
71
+ choices: [{ message: { role: "assistant", content: text }, finish_reason: finishReason }],
72
+ usage: USAGE,
73
+ });
74
+ }
75
+
76
+ export function openaiTool(name: string, args: any, id = "call_1"): Response {
77
+ return Response.json({
78
+ choices: [{
79
+ message: {
80
+ role: "assistant",
81
+ content: null,
82
+ tool_calls: [{ id, type: "function", function: { name, arguments: JSON.stringify(args) } }],
83
+ },
84
+ finish_reason: "tool_calls",
85
+ }],
86
+ usage: USAGE,
87
+ });
88
+ }
89
+
90
+ export function openaiStream(pieces: string[]): Response {
91
+ const events = pieces.map((text) => JSON.stringify({ choices: [{ delta: { content: text } }] }));
92
+ events.push(JSON.stringify({ choices: [], usage: USAGE }));
93
+ events.push(JSON.stringify({ choices: [{ delta: {}, finish_reason: "stop" }] }));
94
+ events.push("[DONE]");
95
+ return sse(events);
96
+ }
97
+
98
+ // ─── Anthropic ───────────────────────────────────────────────────────────
99
+
100
+ export function anthropicText(text: string, stopReason = "end_turn"): Response {
101
+ return Response.json({
102
+ content: [{ type: "text", text }],
103
+ stop_reason: stopReason,
104
+ usage: A_USAGE,
105
+ });
106
+ }
107
+
108
+ export function anthropicTool(name: string, input: any, id = "toolu_1"): Response {
109
+ return Response.json({
110
+ content: [{ type: "tool_use", id, name, input }],
111
+ stop_reason: "tool_use",
112
+ usage: A_USAGE,
113
+ });
114
+ }
115
+
116
+ export function anthropicStream(pieces: string[]): Response {
117
+ const events: string[] = [
118
+ JSON.stringify({ type: "message_start", message: { usage: { input_tokens: 9, output_tokens: 0 } } }),
119
+ JSON.stringify({ type: "content_block_start", index: 0, content_block: { type: "text", text: "" } }),
120
+ ];
121
+ for (const text of pieces) {
122
+ events.push(JSON.stringify({ type: "content_block_delta", index: 0, delta: { type: "text_delta", text } }));
123
+ }
124
+ events.push(JSON.stringify({ type: "content_block_stop", index: 0 }));
125
+ events.push(JSON.stringify({ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 5 } }));
126
+ events.push(JSON.stringify({ type: "message_stop" }));
127
+ return sse(events);
128
+ }
@@ -18,6 +18,9 @@ await bootApp({
18
18
  database: { uri: Bun.env.RO_SRC ?? "mongodb://localhost:27017/_RO_SRC" },
19
19
  replication: {
20
20
  enabled: true,
21
+ // Boot capabilities are under test here, not the replication engine:
22
+ // the scan mode keeps the assertions deterministic
23
+ mode: "scan",
21
24
  runOnBoot: true,
22
25
  schedule: { interval: "1h" },
23
26
  destinations: [{ id: "backup", uri: Bun.env.RO_DST ?? "mongodb://localhost:27017/_RO_DST" }],
@@ -0,0 +1,16 @@
1
+ import { define, agents } from "../../../../index";
2
+
3
+ /**
4
+ * The collection this tenant names for its agent memory. The loader publishes it
5
+ * (`cfg.agentMemories`), so the replication engine copies it like a declared
6
+ * collection — `replication.exclude: ['memory']` is the opt-out.
7
+ */
8
+ export const AGENT_THREADS = "agent_threads";
9
+
10
+ export default define.Agent({
11
+ id: "chat",
12
+ description: "Fixture agent whose memory lives in a tenant-named collection.",
13
+ instructions: "Fixture agent — no model call is ever made in these tests.",
14
+ provider: { model: "test-model", protocol: "openai", apiKey: "fixture" },
15
+ memory: new agents.memory.Mongo({ collection: AGENT_THREADS }),
16
+ });
@@ -0,0 +1,13 @@
1
+ // Test fixture — a collection whose replication key is a **number** (not a date).
2
+ // The delete guard must still match on the destination.
3
+ export default {
4
+ _isCollection_: true,
5
+ _isTimeSerie_: false,
6
+ type: "document",
7
+ slug: "counters",
8
+ fields: [
9
+ { name: "seq", type: "number" },
10
+ { name: "title", type: "string" },
11
+ ],
12
+ replication: { enabled: true, key: "seq" },
13
+ };
@@ -0,0 +1,9 @@
1
+ // Test fixture — a declared collection that opted into replication.
2
+ export default {
3
+ _isCollection_: true,
4
+ _isTimeSerie_: false,
5
+ type: "document",
6
+ slug: "orders",
7
+ fields: [{ name: "title", type: "string" }],
8
+ replication: { enabled: true },
9
+ };
@@ -0,0 +1,8 @@
1
+ // Test fixture — a workflow whose runs must follow the data to the destination.
2
+ export default {
3
+ _isWorkflow_: true,
4
+ id: "run",
5
+ name: "Run",
6
+ version: 1,
7
+ steps: [{ id: "one", exec: async () => ({ ok: true }) }],
8
+ };
@@ -0,0 +1,23 @@
1
+ import { define } from "../../../../index";
2
+
3
+ /**
4
+ * Fixture — an agent whose system prompt carries skills, declared every way a
5
+ * source can be: a glob of a folder, one file, a regex over the tenant, and the
6
+ * `SKILL.md` directory layout.
7
+ *
8
+ * The last two patterns overlap on purpose (`./docs/*.md` and the `docs` regex):
9
+ * a file matched twice is one skill.
10
+ */
11
+ export default define.Agent({
12
+ id: "docs",
13
+ description: "Answers from the tenant's own documentation.",
14
+ instructions: "Answer from the documentation you were given.",
15
+ provider: { model: "claude-test", protocol: "anthropic", apiKey: "fixture-key" },
16
+ skills: [
17
+ "./docs/*.md",
18
+ "./guide/ml.md",
19
+ /^docs\/.*\.md$/,
20
+ /^runbooks\/.*$/,
21
+ "./guides/**/SKILL.md",
22
+ ],
23
+ });
@@ -0,0 +1,14 @@
1
+ import { define } from "../../../../index";
2
+
3
+ /**
4
+ * Fixture — an agent whose skill is malformed (`broken/no-name.md` has no `name`).
5
+ *
6
+ * A broken skill refuses **this agent** at load, and must not hide the others.
7
+ */
8
+ export default define.Agent({
9
+ id: "incomplete",
10
+ description: "Carries a skill that does not follow the format.",
11
+ instructions: "Nothing to see here.",
12
+ provider: { model: "claude-test", protocol: "anthropic", apiKey: "fixture-key" },
13
+ skills: ["./broken/no-name.md"],
14
+ });
@@ -0,0 +1,6 @@
1
+ ---
2
+ name: Bad Name
3
+ description: The name is not kebab-case.
4
+ ---
5
+
6
+ Body.
@@ -0,0 +1,6 @@
1
+ ---
2
+ name: same-name
3
+ description: Two files of one agent cannot share a name — this is the first.
4
+ ---
5
+
6
+ Body.
@@ -0,0 +1,6 @@
1
+ ---
2
+ name: same-name
3
+ description: Two files of one agent cannot share a name — this is the second.
4
+ ---
5
+
6
+ Body.
@@ -0,0 +1,4 @@
1
+ ---
2
+ name: empty-body
3
+ description: Frontmatter only — nothing for the model to read.
4
+ ---
@@ -0,0 +1,7 @@
1
+ ---
2
+ name: long-compat
3
+ description: A skill whose compatibility line is too long.
4
+ compatibility: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
5
+ ---
6
+
7
+ Body.
@@ -0,0 +1,6 @@
1
+ ---
2
+ name: long-description
3
+ description: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
4
+ ---
5
+
6
+ Body.
@@ -0,0 +1,6 @@
1
+ ---
2
+ name: other-name
3
+ description: The parent directory is 'mismatch' — a SKILL.md must be named after it.
4
+ ---
5
+
6
+ Body.
@@ -0,0 +1,5 @@
1
+ ---
2
+ name: no-description
3
+ ---
4
+
5
+ A skill without a description — the model would never know when to read it.
@@ -0,0 +1,5 @@
1
+ ---
2
+ description: A skill without a name — the frontmatter is incomplete.
3
+ ---
4
+
5
+ Body.
@@ -0,0 +1,4 @@
1
+ # No frontmatter at all
2
+
3
+ A plain markdown file is refused: without a name and a description it cannot be
4
+ injected (the model would not know what it is).
@@ -0,0 +1,9 @@
1
+ ---
2
+ name: unknown-keys
3
+ description: Extra frontmatter keys are kept out of the prompt, not refused.
4
+ version: 3
5
+ author: platform
6
+ metadata: { owner: platform }
7
+ ---
8
+
9
+ Body.
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: pdf-forms
3
+ description: Fill, flatten and merge PDF forms. Use it for any AcroForm work.
4
+ license: MIT
5
+ compatibility: Needs the `pdftk` binary on the host.
6
+ metadata:
7
+ owner: platform
8
+ reviewed: 2026-09-24
9
+ allowed-tools: read_pdf write_pdf
10
+ ---
11
+
12
+ # Filling a form
13
+
14
+ 1. Dump the fields: `pdftk form.pdf dump_data_fields`.
15
+ 2. Build the FDF, then fill it: `pdftk form.pdf fill_form data.fdf output out.pdf`.
16
+ 3. Flatten: `pdftk out.pdf output flat.pdf flatten`.
17
+
18
+ Never flatten before the signature step — it makes the form read-only.
@@ -0,0 +1,9 @@
1
+ ---
2
+ name: ml-conventions
3
+ description: The tenant's ML conventions — feature naming, training splits, review gates.
4
+ ---
5
+
6
+ # Conventions
7
+
8
+ - Features are `snake_case`, prefixed by their source (`crm_`, `billing_`).
9
+ - A model only ships with a held-out split and a written evaluation.
@@ -0,0 +1,9 @@
1
+ ---
2
+ name: pdf
3
+ description: Everything about the tenant's PDF pipeline, from templating to archiving.
4
+ ---
5
+
6
+ # PDF pipeline
7
+
8
+ The `name` matches the parent directory, which is what the `SKILL.md` layout
9
+ requires.
@@ -0,0 +1,9 @@
1
+ ---
2
+ name: incident-response
3
+ description: What to do when production breaks — paging, comms, post-mortem.
4
+ ---
5
+
6
+ # Incident response
7
+
8
+ 1. Acknowledge the page within five minutes.
9
+ 2. Open a channel, name an incident commander, keep a timeline.
@@ -0,0 +1,2 @@
1
+ This file is not markdown — a regex that walks the tenant picks it up, and the
2
+ loader must skip it (logged) instead of taking the agent down.
@@ -1,6 +1,9 @@
1
- // Test fixture — a namespace WITHOUT `api.access` (must be denied over HTTP).
1
+ // Test fixture — a namespace WITHOUT `api.access` (must be denied over HTTP),
2
+ // and which explicitly opts OUT of replication.
2
3
  export default {
3
4
  _isVars_: true,
4
5
  namespace: "private",
5
6
  vars: { secret: "shh" },
7
+ // Framework collections are replicated by default: opting out is explicit
8
+ replication: { enabled: false },
6
9
  };
@@ -0,0 +1,34 @@
1
+ // Test fixture — `b` always fails, `a` must be compensated (once, never twice).
2
+ // Also proves the step ctx `rest` is a real tenant client.
3
+ const state = ((globalThis as any).__wfEngine ??= { log: [], attempts: {} });
4
+
5
+ export default {
6
+ _isWorkflow_: true,
7
+ id: "failing",
8
+ name: "Failing",
9
+ version: 1,
10
+ steps: [
11
+ {
12
+ id: "a",
13
+ exec: async ({ rest }: any) => {
14
+ state.log.push(`failing:a:rest=${typeof rest?.find === "function" ? "client" : "engine"}`);
15
+ // A sub-workflow, through the client handed to the step
16
+ await rest.workflow.run("simple", { echo: "sub" });
17
+ state.log.push("failing:a:sub-workflow-done");
18
+ return { a: 1 };
19
+ },
20
+ },
21
+ { id: "b", exec: async () => { state.log.push("failing:b"); throw new Error("boom-b"); } },
22
+ { id: "c", exec: async () => { state.log.push("failing:c"); return { c: 1 }; } },
23
+ ],
24
+ compensations: [
25
+ {
26
+ id: "undo-a",
27
+ depend: ["a"],
28
+ exec: async () => {
29
+ state.attempts["undo-a"] = (state.attempts["undo-a"] ?? 0) + 1;
30
+ state.log.push(`failing:undo-a:${state.attempts["undo-a"]}`);
31
+ },
32
+ },
33
+ ],
34
+ };
@@ -0,0 +1,23 @@
1
+ // Test fixture — a step that fails until `data.failUntil` attempts are reached,
2
+ // with retries. Used to prove a transient failure does not roll the saga back.
3
+ const state = ((globalThis as any).__wfEngine ??= { log: [], attempts: {} });
4
+
5
+ export default {
6
+ _isWorkflow_: true,
7
+ id: "flaky",
8
+ name: "Flaky",
9
+ version: 1,
10
+ steps: [
11
+ {
12
+ id: "unstable",
13
+ retries: 2,
14
+ backoffMs: 1,
15
+ exec: async ({ data }: any) => {
16
+ const n = (state.attempts.unstable = (state.attempts.unstable ?? 0) + 1);
17
+ state.log.push(`flaky:attempt:${n}`);
18
+ if (n <= (data?.failUntil ?? 0)) throw new Error(`transient failure ${n}`);
19
+ return { attempts: n };
20
+ },
21
+ },
22
+ ],
23
+ };
@@ -0,0 +1,17 @@
1
+ // Test fixture — a step that outlives its `timeout` (150ms vs 1s by default).
2
+ export default {
3
+ _isWorkflow_: true,
4
+ id: "hang",
5
+ name: "Hang",
6
+ version: 1,
7
+ steps: [
8
+ {
9
+ id: "hang",
10
+ timeout: "150ms",
11
+ exec: async ({ data }: any) => {
12
+ await new Promise((resolve) => setTimeout(resolve, data?.ms ?? 1000));
13
+ return { done: true };
14
+ },
15
+ },
16
+ ],
17
+ };