@kuralle-agents/core 0.12.0 → 0.14.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 (210) hide show
  1. package/README.md +89 -4
  2. package/dist/ai-sdk/uiMessageStream.d.ts +2 -2
  3. package/dist/ai-sdk/uiMessageStream.js +42 -29
  4. package/dist/capabilities/LivePromptAssembler.d.ts +1 -1
  5. package/dist/capabilities/LivePromptAssembler.js +2 -1
  6. package/dist/capabilities/index.d.ts +1 -1
  7. package/dist/eval/EvalRunner.js +4 -4
  8. package/dist/eval/simulation.js +4 -3
  9. package/dist/events/TurnHandle.d.ts +3 -3
  10. package/dist/flow/collectDigression.d.ts +5 -2
  11. package/dist/flow/collectDigression.js +48 -10
  12. package/dist/flow/collectUntilComplete.js +13 -5
  13. package/dist/flow/degrade.js +8 -4
  14. package/dist/flow/emitInteractive.d.ts +2 -2
  15. package/dist/flow/emitInteractive.js +6 -3
  16. package/dist/flow/extraction.d.ts +3 -2
  17. package/dist/flow/extraction.js +14 -4
  18. package/dist/flow/reduceTransition.d.ts +2 -2
  19. package/dist/flow/reduceTransition.js +7 -3
  20. package/dist/flow/runFlow.js +97 -17
  21. package/dist/foundation/AgentStateController.d.ts +1 -1
  22. package/dist/foundation/ToolExecutor.d.ts +2 -2
  23. package/dist/foundation/index.d.ts +0 -11
  24. package/dist/foundation/index.js +0 -5
  25. package/dist/index.d.ts +15 -15
  26. package/dist/index.js +9 -6
  27. package/dist/outcomes/streamPart.d.ts +2 -2
  28. package/dist/outcomes/streamPart.js +2 -3
  29. package/dist/prompts/PromptBuilder.d.ts +3 -3
  30. package/dist/prompts/PromptBuilder.js +13 -6
  31. package/dist/prompts/index.d.ts +1 -1
  32. package/dist/prompts/types.d.ts +9 -2
  33. package/dist/runtime/InMemoryRetrievalCache.d.ts +25 -0
  34. package/dist/runtime/InMemoryRetrievalCache.js +59 -0
  35. package/dist/runtime/KnowledgeProvider.d.ts +3 -2
  36. package/dist/runtime/KnowledgeProvider.js +60 -31
  37. package/dist/runtime/Runtime.d.ts +47 -1
  38. package/dist/runtime/Runtime.js +273 -36
  39. package/dist/runtime/TokenAccumulator.d.ts +7 -0
  40. package/dist/runtime/TokenAccumulator.js +7 -0
  41. package/dist/runtime/TraceRecorder.d.ts +51 -0
  42. package/dist/runtime/TraceRecorder.js +326 -0
  43. package/dist/runtime/buildAgentToolSurface.js +1 -1
  44. package/dist/runtime/channels/TextDriver.js +57 -31
  45. package/dist/runtime/channels/executeModelTool.d.ts +14 -1
  46. package/dist/runtime/channels/executeModelTool.js +132 -3
  47. package/dist/runtime/channels/extractionTurn.js +1 -1
  48. package/dist/runtime/channels/index.d.ts +0 -3
  49. package/dist/runtime/channels/index.js +0 -6
  50. package/dist/runtime/channels/inputBuffer.d.ts +1 -0
  51. package/dist/runtime/channels/inputBuffer.js +9 -0
  52. package/dist/runtime/channels/streaming/hostControlSpeak.js +5 -1
  53. package/dist/runtime/channels/streaming/speakGated.js +24 -12
  54. package/dist/runtime/citations/index.d.ts +3 -3
  55. package/dist/runtime/closeRun.js +14 -10
  56. package/dist/runtime/compaction.d.ts +2 -0
  57. package/dist/runtime/compaction.js +3 -2
  58. package/dist/runtime/controlFlowSignal.d.ts +24 -0
  59. package/dist/runtime/controlFlowSignal.js +29 -0
  60. package/dist/runtime/ctx.d.ts +3 -4
  61. package/dist/runtime/ctx.js +147 -60
  62. package/dist/runtime/durable/RunStore.d.ts +16 -0
  63. package/dist/runtime/durable/RunStore.js +7 -1
  64. package/dist/runtime/durable/SessionRunStore.d.ts +7 -2
  65. package/dist/runtime/durable/SessionRunStore.js +136 -34
  66. package/dist/runtime/durable/idempotency.d.ts +1 -0
  67. package/dist/runtime/durable/idempotency.js +3 -0
  68. package/dist/runtime/durable/replay.js +7 -2
  69. package/dist/runtime/durable/types.d.ts +11 -0
  70. package/dist/runtime/goals.d.ts +18 -0
  71. package/dist/runtime/goals.js +158 -0
  72. package/dist/runtime/grounding/gather.d.ts +1 -1
  73. package/dist/runtime/grounding/inMemoryKnowledge.d.ts +1 -1
  74. package/dist/runtime/grounding/knowledge.d.ts +1 -1
  75. package/dist/runtime/grounding/knowledge.js +1 -1
  76. package/dist/runtime/handoffContinuation.d.ts +20 -0
  77. package/dist/runtime/handoffContinuation.js +30 -0
  78. package/dist/runtime/handoffOscillation.d.ts +6 -0
  79. package/dist/runtime/handoffOscillation.js +17 -0
  80. package/dist/runtime/hostLoop.js +37 -6
  81. package/dist/runtime/index.d.ts +4 -3
  82. package/dist/runtime/index.js +2 -1
  83. package/dist/runtime/openRun.d.ts +2 -0
  84. package/dist/runtime/openRun.js +46 -17
  85. package/dist/runtime/outcomeMarking.d.ts +2 -2
  86. package/dist/runtime/outcomeMarking.js +2 -1
  87. package/dist/runtime/policies/agentTurn.d.ts +1 -1
  88. package/dist/runtime/policies/limits.d.ts +1 -0
  89. package/dist/runtime/policies/limits.js +3 -0
  90. package/dist/runtime/runHookSafely.d.ts +3 -0
  91. package/dist/runtime/runHookSafely.js +19 -0
  92. package/dist/runtime/select.d.ts +5 -1
  93. package/dist/runtime/select.js +30 -1
  94. package/dist/runtime/turnTokenUsage.d.ts +28 -0
  95. package/dist/runtime/turnTokenUsage.js +70 -0
  96. package/dist/scheduler/index.d.ts +2 -2
  97. package/dist/scheduler/index.js +1 -1
  98. package/dist/services/TracingService.js +2 -3
  99. package/dist/session/SessionStore.d.ts +6 -0
  100. package/dist/session/SessionStore.js +12 -1
  101. package/dist/session/stores/MemoryStore.d.ts +1 -1
  102. package/dist/session/stores/MemoryStore.js +16 -2
  103. package/dist/session/testing.d.ts +6 -1
  104. package/dist/session/testing.js +34 -0
  105. package/dist/session/utils.d.ts +3 -0
  106. package/dist/session/utils.js +23 -0
  107. package/dist/skills/collectSkills.d.ts +11 -3
  108. package/dist/skills/collectSkills.js +51 -8
  109. package/dist/skills/compositeSkillStore.d.ts +26 -0
  110. package/dist/skills/compositeSkillStore.js +54 -0
  111. package/dist/skills/defineSkill.d.ts +21 -0
  112. package/dist/skills/defineSkill.js +26 -0
  113. package/dist/skills/fsSkillStore.d.ts +3 -0
  114. package/dist/skills/fsSkillStore.js +79 -0
  115. package/dist/skills/index.d.ts +4 -0
  116. package/dist/skills/index.js +4 -0
  117. package/dist/skills/parseSkillFrontmatter.d.ts +14 -0
  118. package/dist/skills/parseSkillFrontmatter.js +183 -0
  119. package/dist/skills/wireAgentSkills.d.ts +2 -1
  120. package/dist/skills/wireAgentSkills.js +2 -2
  121. package/dist/testing/mocks.d.ts +3 -3
  122. package/dist/testing/mocks.js +7 -3
  123. package/dist/tools/controlResults.d.ts +14 -0
  124. package/dist/tools/controlResults.js +15 -0
  125. package/dist/tools/effect/ToolExecutor.d.ts +6 -0
  126. package/dist/tools/effect/ToolExecutor.js +75 -36
  127. package/dist/tools/effect/defineTool.d.ts +11 -0
  128. package/dist/tools/effect/defineTool.js +3 -0
  129. package/dist/tools/effect/errors.d.ts +2 -5
  130. package/dist/tools/effect/errors.js +2 -5
  131. package/dist/tools/index.d.ts +0 -1
  132. package/dist/tools/index.js +0 -1
  133. package/dist/tracing/MemoryTraceStore.d.ts +16 -0
  134. package/dist/tracing/MemoryTraceStore.js +56 -0
  135. package/dist/tracing/OtelTraceSink.d.ts +127 -0
  136. package/dist/tracing/OtelTraceSink.js +101 -0
  137. package/dist/tracing/TraceStore.d.ts +19 -0
  138. package/dist/tracing/TraceStore.js +32 -0
  139. package/dist/tracing/index.d.ts +3 -0
  140. package/dist/tracing/index.js +3 -0
  141. package/dist/tracing/testing.d.ts +3 -0
  142. package/dist/tracing/testing.js +45 -0
  143. package/dist/types/agentConfig.d.ts +2 -1
  144. package/dist/types/channel.d.ts +14 -1
  145. package/dist/types/effectTool.d.ts +6 -0
  146. package/dist/types/flow.d.ts +1 -1
  147. package/dist/types/guardrails.d.ts +5 -0
  148. package/dist/types/hooks.d.ts +2 -2
  149. package/dist/types/index.d.ts +25 -15
  150. package/dist/types/index.js +8 -12
  151. package/dist/types/knowledge.d.ts +213 -0
  152. package/dist/types/processors.d.ts +0 -58
  153. package/dist/types/run-context.d.ts +16 -7
  154. package/dist/types/runtime.d.ts +3 -74
  155. package/dist/types/session.d.ts +11 -1
  156. package/dist/types/skills.d.ts +15 -1
  157. package/dist/types/stream.d.ts +138 -68
  158. package/dist/types/stream.js +34 -1
  159. package/dist/types/trace.d.ts +52 -0
  160. package/guides/AGENTS.md +3 -3
  161. package/guides/EXAMPLE_VERIFICATION.md +4 -4
  162. package/guides/FLOWS.md +1 -1
  163. package/guides/GETTING_STARTED.md +1 -1
  164. package/guides/RUNTIME.md +20 -15
  165. package/guides/TOOLS.md +1 -1
  166. package/package.json +13 -14
  167. package/dist/foundation/AgentDefinition.d.ts +0 -17
  168. package/dist/foundation/ConversationEventLog.d.ts +0 -71
  169. package/dist/foundation/ConversationState.d.ts +0 -33
  170. package/dist/foundation/ConversationState.js +0 -1
  171. package/dist/foundation/DefaultConversationEventLog.d.ts +0 -27
  172. package/dist/foundation/DefaultConversationEventLog.js +0 -205
  173. package/dist/foundation/DefaultConversationState.d.ts +0 -36
  174. package/dist/foundation/DefaultConversationState.js +0 -103
  175. package/dist/foundation/DefaultToolExecutor.d.ts +0 -57
  176. package/dist/foundation/DefaultToolExecutor.js +0 -132
  177. package/dist/foundation/createFoundation.d.ts +0 -32
  178. package/dist/foundation/createFoundation.js +0 -33
  179. package/dist/hooks/HookRunner.d.ts +0 -37
  180. package/dist/hooks/HookRunner.js +0 -111
  181. package/dist/hooks/builtin/logging.d.ts +0 -5
  182. package/dist/hooks/builtin/logging.js +0 -98
  183. package/dist/hooks/builtin/metrics.d.ts +0 -7
  184. package/dist/hooks/builtin/metrics.js +0 -60
  185. package/dist/hooks/builtin/observability.d.ts +0 -20
  186. package/dist/hooks/builtin/observability.js +0 -572
  187. package/dist/hooks/helpers.d.ts +0 -22
  188. package/dist/hooks/helpers.js +0 -183
  189. package/dist/hooks/index.d.ts +0 -8
  190. package/dist/hooks/index.js +0 -6
  191. package/dist/realtime/RealtimeAudioClient.d.ts +0 -166
  192. package/dist/realtime/RealtimeAudioClient.js +0 -14
  193. package/dist/realtime/RealtimeSessionHandle.d.ts +0 -6
  194. package/dist/realtime/RealtimeSessionHandle.js +0 -1
  195. package/dist/realtime/index.d.ts +0 -2
  196. package/dist/realtime/index.js +0 -1
  197. package/dist/runtime/channels/VoiceDriver.d.ts +0 -31
  198. package/dist/runtime/channels/VoiceDriver.js +0 -321
  199. package/dist/runtime/channels/voiceTools.d.ts +0 -9
  200. package/dist/runtime/channels/voiceTools.js +0 -51
  201. package/dist/tools/errorHandling.d.ts +0 -35
  202. package/dist/tools/errorHandling.js +0 -145
  203. package/dist/types/voice.d.ts +0 -517
  204. package/dist/types/voice.js +0 -14
  205. package/dist/utils/isRecord.d.ts +0 -1
  206. package/dist/utils/isRecord.js +0 -3
  207. package/dist/utils/messageNormalization.d.ts +0 -2
  208. package/dist/utils/messageNormalization.js +0 -137
  209. /package/dist/{foundation/AgentDefinition.js → types/knowledge.js} +0 -0
  210. /package/dist/{foundation/ConversationEventLog.js → types/trace.js} +0 -0
@@ -21,6 +21,7 @@
21
21
  * `bun:test` into runtime bundles.
22
22
  */
23
23
  import { describe, test, expect, beforeEach } from 'bun:test';
24
+ import { StaleWriteError } from './SessionStore.js';
24
25
  const buildSession = (overrides = {}) => {
25
26
  const now = new Date();
26
27
  return {
@@ -120,3 +121,36 @@ export function runSessionStoreContract(factory) {
120
121
  });
121
122
  });
122
123
  }
124
+ /**
125
+ * Registers optimistic-concurrency (CAS) contract tests for SessionStore adapters.
126
+ * Two concurrent saves with the same expected version: exactly one succeeds.
127
+ */
128
+ export function runSessionStoreCasContract(factory) {
129
+ describe('SessionStore CAS contract', () => {
130
+ let store;
131
+ beforeEach(async () => {
132
+ store = await factory();
133
+ });
134
+ test('concurrent save with same expected version rejects one writer with StaleWriteError', async () => {
135
+ const session = buildSession({ id: 'cas-sess' });
136
+ await store.save(session);
137
+ const firstRead = (await store.get('cas-sess'));
138
+ const secondRead = (await store.get('cas-sess'));
139
+ expect(firstRead.version).toBe(1);
140
+ expect(secondRead.version).toBe(1);
141
+ firstRead.workingMemory = { writer: 'a' };
142
+ secondRead.workingMemory = { writer: 'b' };
143
+ const results = await Promise.allSettled([
144
+ store.save(firstRead),
145
+ store.save(secondRead),
146
+ ]);
147
+ const fulfilled = results.filter((r) => r.status === 'fulfilled');
148
+ const rejected = results.filter((r) => r.status === 'rejected');
149
+ expect(fulfilled).toHaveLength(1);
150
+ expect(rejected).toHaveLength(1);
151
+ expect(rejected[0].reason).toBeInstanceOf(StaleWriteError);
152
+ const final = await store.get('cas-sess');
153
+ expect(final?.version).toBe(2);
154
+ });
155
+ });
156
+ }
@@ -1,4 +1,5 @@
1
1
  import type { Session } from '../types/index.js';
2
+ import { type SessionStore } from './SessionStore.js';
2
3
  /**
3
4
  * Restores a Session from its serialized form.
4
5
  *
@@ -17,3 +18,5 @@ import type { Session } from '../types/index.js';
17
18
  * untouched in the key fields — callers are expected to have validated shape.
18
19
  */
19
20
  export declare function reviveSession(raw: unknown): Session;
21
+ /** Apply a session mutation to the latest snapshot and retry it after CAS conflicts. */
22
+ export declare function mutateSessionWithRetry(store: SessionStore, sessionId: string, mutator: (session: Session) => void | Promise<void>): Promise<Session>;
@@ -1,3 +1,5 @@
1
+ import { StaleWriteError } from './SessionStore.js';
2
+ const CAS_RETRIES = 8;
1
3
  /**
2
4
  * Restores a Session from its serialized form.
3
5
  *
@@ -45,3 +47,24 @@ export function reviveSession(raw) {
45
47
  ]));
46
48
  return session;
47
49
  }
50
+ /** Apply a session mutation to the latest snapshot and retry it after CAS conflicts. */
51
+ export async function mutateSessionWithRetry(store, sessionId, mutator) {
52
+ for (let attempt = 0; attempt < CAS_RETRIES; attempt++) {
53
+ const session = await store.get(sessionId);
54
+ if (!session) {
55
+ throw new Error(`Session not found: ${sessionId}`);
56
+ }
57
+ await mutator(session);
58
+ try {
59
+ await store.save(session);
60
+ return (await store.get(sessionId)) ?? session;
61
+ }
62
+ catch (error) {
63
+ if (error instanceof StaleWriteError && attempt < CAS_RETRIES - 1) {
64
+ continue;
65
+ }
66
+ throw error;
67
+ }
68
+ }
69
+ throw new Error(`CAS retry limit exceeded for session: ${sessionId}`);
70
+ }
@@ -1,5 +1,6 @@
1
1
  import type { AnyTool } from '../types/effectTool.js';
2
- import type { SkillLike, SkillSource, SkillStoreLike } from '../types/skills.js';
2
+ import type { FileSystem } from '../types/filesystem.js';
3
+ import type { SkillEntry, SkillLike, SkillSource, SkillStoreLike } from '../types/skills.js';
3
4
  export interface SkillWireAgent {
4
5
  skills?: SkillSource;
5
6
  tools?: Record<string, AnyTool>;
@@ -8,10 +9,17 @@ export interface SkillWireAgent {
8
9
  name: string;
9
10
  }>;
10
11
  }
11
- export declare function isSkillStore(value: SkillSource): value is SkillStoreLike;
12
+ export declare function isSkillStore(value: SkillEntry | SkillSource): value is SkillStoreLike;
12
13
  export declare function collectRegisteredNames(agent: SkillWireAgent): Set<string>;
13
14
  export declare function validateSkillAllowedTools(skills: SkillLike[], registered: Set<string>): void;
14
- export declare function prepareSkillStore(source: SkillSource): Promise<{
15
+ /**
16
+ * Turns whatever the author wrote in `AgentConfig.skills` into one store.
17
+ *
18
+ * `fs` is the agent's workspace filesystem, needed only to resolve `string` entries. A path
19
+ * without a workspace is an authoring mistake, so it throws rather than silently yielding no
20
+ * skills — a missing skill is invisible at runtime and looks like the model ignoring it.
21
+ */
22
+ export declare function prepareSkillStore(source: SkillSource, fs?: FileSystem): Promise<{
15
23
  store: SkillStoreLike;
16
24
  skills: SkillLike[];
17
25
  }>;
@@ -1,4 +1,6 @@
1
1
  import { InlineSkillStore } from './inlineSkillStore.js';
2
+ import { CompositeSkillStore } from './compositeSkillStore.js';
3
+ import { fsSkillStore } from './fsSkillStore.js';
2
4
  export function isSkillStore(value) {
3
5
  return (typeof value === 'object' &&
4
6
  value !== null &&
@@ -7,9 +9,6 @@ export function isSkillStore(value) {
7
9
  typeof value.list === 'function');
8
10
  }
9
11
  async function collectSkillsFromSource(source) {
10
- if (!isSkillStore(source)) {
11
- return Array.isArray(source) ? source : [source];
12
- }
13
12
  if (typeof source.getAllSkills === 'function') {
14
13
  const all = source.getAllSkills();
15
14
  return Array.isArray(all) ? all : await all;
@@ -47,10 +46,54 @@ export function validateSkillAllowedTools(skills, registered) {
47
46
  }
48
47
  }
49
48
  }
50
- export async function prepareSkillStore(source) {
51
- if (isSkillStore(source)) {
52
- return { store: source, skills: await collectSkillsFromSource(source) };
49
+ /**
50
+ * Turns whatever the author wrote in `AgentConfig.skills` into one store.
51
+ *
52
+ * `fs` is the agent's workspace filesystem, needed only to resolve `string` entries. A path
53
+ * without a workspace is an authoring mistake, so it throws rather than silently yielding no
54
+ * skills — a missing skill is invisible at runtime and looks like the model ignoring it.
55
+ */
56
+ export async function prepareSkillStore(source, fs) {
57
+ const entries = Array.isArray(source)
58
+ ? [...source]
59
+ : [source];
60
+ // A lone store stays itself: wrapping one store in a composite would add a layer with
61
+ // nothing to compose, and lose any extra capability the store exposes.
62
+ if (entries.length === 1 && isSkillStore(entries[0])) {
63
+ const only = entries[0];
64
+ return { store: only, skills: await collectSkillsFromSource(only) };
65
+ }
66
+ const stores = [];
67
+ let inline = [];
68
+ const flushInline = () => {
69
+ if (inline.length > 0) {
70
+ stores.push(new InlineSkillStore(inline));
71
+ inline = [];
72
+ }
73
+ };
74
+ for (const entry of entries) {
75
+ if (typeof entry === 'string') {
76
+ if (!fs) {
77
+ throw new Error(`[skills] "${entry}" is a workspace path, but this agent has no \`workspace\` filesystem. ` +
78
+ 'Set `workspace` on the agent, or pass skills inline with defineSkill().');
79
+ }
80
+ flushInline();
81
+ stores.push(fsSkillStore(fs, [entry]));
82
+ continue;
83
+ }
84
+ if (isSkillStore(entry)) {
85
+ flushInline();
86
+ stores.push(entry);
87
+ continue;
88
+ }
89
+ // Consecutive inline skills share one store so ordering within a run is preserved.
90
+ inline.push(entry);
91
+ }
92
+ flushInline();
93
+ if (stores.length === 1) {
94
+ const only = stores[0];
95
+ return { store: only, skills: await collectSkillsFromSource(only) };
53
96
  }
54
- const skills = Array.isArray(source) ? source : [source];
55
- return { store: new InlineSkillStore(skills), skills };
97
+ const store = new CompositeSkillStore(stores);
98
+ return { store, skills: await collectSkillsFromSource(store) };
56
99
  }
@@ -0,0 +1,26 @@
1
+ import type { SkillMeta, SkillStoreLike } from '../types/skills.js';
2
+ /**
3
+ * Presents several skill stores as one, with **later stores winning** a name collision.
4
+ *
5
+ * Delegates rather than flattening on purpose: flattening would force every body and
6
+ * resource to load up front, which is exactly what progressive disclosure exists to avoid.
7
+ * Only `list()` (Level 1 — names and descriptions) is eager; bodies and resources stay
8
+ * behind their own calls.
9
+ */
10
+ export declare class CompositeSkillStore implements SkillStoreLike {
11
+ private readonly stores;
12
+ /** Name → the store that owns it. Built on first `list()`, reused after. */
13
+ private owners?;
14
+ /** @param stores In precedence order, lowest first. */
15
+ constructor(stores: readonly SkillStoreLike[]);
16
+ list(): Promise<SkillMeta[]>;
17
+ loadBody(name: string): Promise<string>;
18
+ loadResource(name: string, path: string): Promise<string | Uint8Array>;
19
+ loadAllSkills(): Promise<{
20
+ name: string;
21
+ description: string;
22
+ body: string;
23
+ }[]>;
24
+ private ownerOf;
25
+ private resolveOwners;
26
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Presents several skill stores as one, with **later stores winning** a name collision.
3
+ *
4
+ * Delegates rather than flattening on purpose: flattening would force every body and
5
+ * resource to load up front, which is exactly what progressive disclosure exists to avoid.
6
+ * Only `list()` (Level 1 — names and descriptions) is eager; bodies and resources stay
7
+ * behind their own calls.
8
+ */
9
+ export class CompositeSkillStore {
10
+ stores;
11
+ /** Name → the store that owns it. Built on first `list()`, reused after. */
12
+ owners;
13
+ /** @param stores In precedence order, lowest first. */
14
+ constructor(stores) {
15
+ this.stores = stores;
16
+ }
17
+ async list() {
18
+ const merged = new Map();
19
+ const owners = new Map();
20
+ for (const store of this.stores) {
21
+ for (const meta of await store.list()) {
22
+ merged.set(meta.name, meta);
23
+ owners.set(meta.name, store);
24
+ }
25
+ }
26
+ this.owners = owners;
27
+ return [...merged.values()].sort((a, b) => a.name.localeCompare(b.name));
28
+ }
29
+ async loadBody(name) {
30
+ return (await this.ownerOf(name)).loadBody(name);
31
+ }
32
+ async loadResource(name, path) {
33
+ return (await this.ownerOf(name)).loadResource(name, path);
34
+ }
35
+ async loadAllSkills() {
36
+ const owners = await this.resolveOwners();
37
+ const out = [];
38
+ for (const [name, store] of owners) {
39
+ out.push({ name, description: '', body: await store.loadBody(name) });
40
+ }
41
+ return out;
42
+ }
43
+ async ownerOf(name) {
44
+ const owner = (await this.resolveOwners()).get(name);
45
+ if (!owner)
46
+ throw new Error(`[skills] Skill "${name}" not found.`);
47
+ return owner;
48
+ }
49
+ async resolveOwners() {
50
+ if (!this.owners)
51
+ await this.list();
52
+ return this.owners;
53
+ }
54
+ }
@@ -0,0 +1,21 @@
1
+ import type { SkillLike } from '../types/skills.js';
2
+ export interface DefineSkillConfig {
3
+ /** Lowercase, hyphen-separated, ≤64 chars — the Agent Skills naming rule. */
4
+ name: string;
5
+ /** When the model should reach for this skill. Disclosed up front, so keep it specific. ≤1024 chars. */
6
+ description: string;
7
+ /** The skill body — Level 2, loaded only when the model calls `load_skill`. */
8
+ instructions: string;
9
+ /** Level 3: fetched individually via `read_skill_resource`, never loaded up front. */
10
+ resources?: Record<string, string | Uint8Array>;
11
+ /** Restricts this skill to a subset of the agent's tools. Unknown names fail at wiring time. */
12
+ allowedTools?: string[];
13
+ }
14
+ /**
15
+ * Authors a skill inline, with no filesystem involved.
16
+ *
17
+ * Validated against the same rules as a `SKILL.md` on disk — an inline skill that would be
18
+ * rejected as a file is rejected here too, so moving one to `agents/<id>/skills/` later
19
+ * cannot start failing.
20
+ */
21
+ export declare function defineSkill(config: DefineSkillConfig): SkillLike;
@@ -0,0 +1,26 @@
1
+ import { validateSkillDescription, validateSkillName } from './parseSkillFrontmatter.js';
2
+ /**
3
+ * Authors a skill inline, with no filesystem involved.
4
+ *
5
+ * Validated against the same rules as a `SKILL.md` on disk — an inline skill that would be
6
+ * rejected as a file is rejected here too, so moving one to `agents/<id>/skills/` later
7
+ * cannot start failing.
8
+ */
9
+ export function defineSkill(config) {
10
+ const name = config.name?.trim();
11
+ const description = config.description?.trim();
12
+ if (!name)
13
+ throw new Error('[skills] defineSkill requires a non-empty name.');
14
+ if (!description) {
15
+ throw new Error(`[skills] Skill "${name}" requires a non-empty description.`);
16
+ }
17
+ validateSkillName(name, name);
18
+ validateSkillDescription(description, name);
19
+ return {
20
+ name,
21
+ description,
22
+ body: config.instructions,
23
+ resources: config.resources,
24
+ allowedTools: config.allowedTools,
25
+ };
26
+ }
@@ -0,0 +1,3 @@
1
+ import type { FileSystem } from '../types/filesystem.js';
2
+ import type { SkillStoreLike } from '../types/skills.js';
3
+ export declare function fsSkillStore(fs: FileSystem, orderedRoots?: readonly string[]): SkillStoreLike;
@@ -0,0 +1,79 @@
1
+ import { parseSkillFrontmatter } from './parseSkillFrontmatter.js';
2
+ const DEFAULT_ROOT = '/skills';
3
+ export function fsSkillStore(fs, orderedRoots = [DEFAULT_ROOT]) {
4
+ const roots = [...orderedRoots];
5
+ return {
6
+ async list() {
7
+ const skills = await discoverSkills(fs, roots, true);
8
+ return [...skills.values()]
9
+ .map((skill) => skill.meta)
10
+ .sort((a, b) => a.name.localeCompare(b.name));
11
+ },
12
+ async loadBody(name) {
13
+ const skill = (await discoverSkills(fs, roots, false)).get(name);
14
+ if (!skill) {
15
+ throw new Error(`[skills] Skill "${name}" not found.`);
16
+ }
17
+ return skill.body;
18
+ },
19
+ async loadResource(name, path) {
20
+ const skill = (await discoverSkills(fs, roots, false)).get(name);
21
+ if (!skill) {
22
+ throw new Error(`[skills] Skill "${name}" not found.`);
23
+ }
24
+ const normalized = path.trim().replace(/^\.?\//, '');
25
+ if (normalized.includes('..') || normalized.startsWith('/')) {
26
+ throw new Error(`[skills] Invalid resource path "${path}".`);
27
+ }
28
+ const resourcePath = fs.resolvePath(skill.root, `${skill.folder}/${normalized}`);
29
+ if (!(await fs.exists(resourcePath))) {
30
+ const err = new Error(`ENOENT: [skills] Resource "${normalized}" not found for skill "${name}".`);
31
+ throw err;
32
+ }
33
+ return fs.readFile(resourcePath);
34
+ },
35
+ };
36
+ }
37
+ async function discoverSkills(fs, roots, warnInvalid) {
38
+ const skills = new Map();
39
+ for (const root of roots) {
40
+ let entries;
41
+ try {
42
+ entries = await fs.readdir(root);
43
+ }
44
+ catch {
45
+ continue;
46
+ }
47
+ for (const entry of entries.sort()) {
48
+ const entryPath = fs.resolvePath(root, entry);
49
+ let stat;
50
+ try {
51
+ stat = await fs.stat(entryPath);
52
+ }
53
+ catch {
54
+ continue;
55
+ }
56
+ if (stat.type !== 'directory')
57
+ continue;
58
+ const skillPath = fs.resolvePath(root, `${entry}/SKILL.md`);
59
+ if (!(await fs.exists(skillPath)))
60
+ continue;
61
+ try {
62
+ const content = await fs.readFile(skillPath);
63
+ const parsed = parseSkillFrontmatter(content, { path: skillPath });
64
+ skills.set(parsed.name, {
65
+ root,
66
+ folder: entry,
67
+ meta: { name: parsed.name, description: parsed.description },
68
+ body: parsed.body,
69
+ });
70
+ }
71
+ catch (err) {
72
+ if (warnInvalid) {
73
+ console.warn(`[skills] Skipping ${skillPath}: ${err instanceof Error ? err.message : String(err)}`);
74
+ }
75
+ }
76
+ }
77
+ }
78
+ return skills;
79
+ }
@@ -3,3 +3,7 @@ export { wireAgentSkills } from './wireAgentSkills.js';
3
3
  export type { WiredAgentSkills } from './wireAgentSkills.js';
4
4
  export { collectRegisteredNames, validateSkillAllowedTools, prepareSkillStore, isSkillStore, type SkillWireAgent, } from './collectSkills.js';
5
5
  export { InlineSkillStore } from './inlineSkillStore.js';
6
+ export { CompositeSkillStore } from './compositeSkillStore.js';
7
+ export { fsSkillStore } from './fsSkillStore.js';
8
+ export { defineSkill, type DefineSkillConfig } from './defineSkill.js';
9
+ export { parseSkillFrontmatter, validateSkillName, validateSkillDescription, type ParsedSkill, } from './parseSkillFrontmatter.js';
@@ -2,3 +2,7 @@ export { SkillsCapability } from './SkillsCapability.js';
2
2
  export { wireAgentSkills } from './wireAgentSkills.js';
3
3
  export { collectRegisteredNames, validateSkillAllowedTools, prepareSkillStore, isSkillStore, } from './collectSkills.js';
4
4
  export { InlineSkillStore } from './inlineSkillStore.js';
5
+ export { CompositeSkillStore } from './compositeSkillStore.js';
6
+ export { fsSkillStore } from './fsSkillStore.js';
7
+ export { defineSkill } from './defineSkill.js';
8
+ export { parseSkillFrontmatter, validateSkillName, validateSkillDescription, } from './parseSkillFrontmatter.js';
@@ -0,0 +1,14 @@
1
+ export interface ParsedSkill {
2
+ name: string;
3
+ description: string;
4
+ body: string;
5
+ license?: string;
6
+ compatibility?: string;
7
+ allowedTools?: string[];
8
+ metadata?: Record<string, string>;
9
+ }
10
+ export declare function parseSkillFrontmatter(content: string, ctx: {
11
+ path: string;
12
+ }): ParsedSkill;
13
+ export declare function validateSkillName(name: string, path: string): void;
14
+ export declare function validateSkillDescription(description: string, path: string): void;
@@ -0,0 +1,183 @@
1
+ const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)([\s\S]*)$/;
2
+ const NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
3
+ export function parseSkillFrontmatter(content, ctx) {
4
+ const stripped = content.replace(/^\uFEFF/, '');
5
+ const match = stripped.match(FRONTMATTER_RE);
6
+ if (!match) {
7
+ throw new Error(`[skills] Skill ${ctx.path} is missing YAML frontmatter. Start SKILL.md with "---", ` +
8
+ 'include "name" and "description", then close the block with "---".');
9
+ }
10
+ const raw = parseFlatYaml(match[1] ?? '', ctx.path);
11
+ const name = requireField(raw, 'name', ctx.path);
12
+ const description = requireField(raw, 'description', ctx.path);
13
+ validateSkillName(name, ctx.path);
14
+ validateSkillDescription(description, ctx.path);
15
+ const compatibility = optionalString(raw.compatibility);
16
+ if (compatibility !== undefined && codePointLength(compatibility) > 500) {
17
+ throw new Error(`[skills] Skill ${ctx.path} field "compatibility" exceeds 500 characters.`);
18
+ }
19
+ let body = match[2] ?? '';
20
+ if (body.startsWith('\n')) {
21
+ body = body.slice(1);
22
+ }
23
+ const result = { name, description, body };
24
+ const license = optionalString(raw.license);
25
+ if (license !== undefined)
26
+ result.license = license;
27
+ if (compatibility !== undefined)
28
+ result.compatibility = compatibility;
29
+ const allowedTools = parseAllowedTools(raw['allowed-tools']);
30
+ if (allowedTools !== undefined)
31
+ result.allowedTools = allowedTools;
32
+ const metadata = parseMetadata(raw.metadata);
33
+ if (metadata !== undefined)
34
+ result.metadata = metadata;
35
+ return result;
36
+ }
37
+ function parseFlatYaml(yaml, path) {
38
+ const result = {};
39
+ const lines = yaml.split(/\r?\n/);
40
+ let i = 0;
41
+ while (i < lines.length) {
42
+ const line = lines[i];
43
+ if (line.trim() === '') {
44
+ i += 1;
45
+ continue;
46
+ }
47
+ const keyMatch = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
48
+ if (!keyMatch) {
49
+ throw new Error(`[skills] Skill ${path} has invalid YAML frontmatter near line: ${line}`);
50
+ }
51
+ const key = keyMatch[1];
52
+ const rest = (keyMatch[2] ?? '').trim();
53
+ if (rest === '') {
54
+ i += 1;
55
+ if (i >= lines.length)
56
+ break;
57
+ const next = lines[i];
58
+ if (next.match(/^\s+-\s+/)) {
59
+ const items = [];
60
+ while (i < lines.length) {
61
+ const listLine = lines[i];
62
+ const itemMatch = listLine.match(/^\s+-\s+(.*)$/);
63
+ if (!itemMatch)
64
+ break;
65
+ items.push(parseScalar(itemMatch[1] ?? ''));
66
+ i += 1;
67
+ }
68
+ result[key] = items;
69
+ continue;
70
+ }
71
+ if (key === 'metadata') {
72
+ const meta = {};
73
+ while (i < lines.length) {
74
+ const metaLine = lines[i];
75
+ const metaMatch = metaLine.match(/^\s{2}([A-Za-z0-9_-]+):\s*(.*)$/);
76
+ if (!metaMatch)
77
+ break;
78
+ meta[metaMatch[1]] = parseScalar((metaMatch[2] ?? '').trim());
79
+ i += 1;
80
+ }
81
+ result[key] = meta;
82
+ continue;
83
+ }
84
+ result[key] = '';
85
+ continue;
86
+ }
87
+ if (rest.startsWith('[') && rest.endsWith(']')) {
88
+ const inner = rest.slice(1, -1).trim();
89
+ result[key] =
90
+ inner === ''
91
+ ? []
92
+ : inner.split(',').map((s) => parseScalar(s.trim()));
93
+ i += 1;
94
+ continue;
95
+ }
96
+ result[key] = parseScalar(rest);
97
+ i += 1;
98
+ }
99
+ return result;
100
+ }
101
+ function parseScalar(value) {
102
+ const trimmed = value.trim();
103
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
104
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
105
+ return trimmed.slice(1, -1);
106
+ }
107
+ return trimmed;
108
+ }
109
+ function requireField(raw, field, path) {
110
+ const value = raw[field];
111
+ if (typeof value !== 'string' || value.trim().length === 0) {
112
+ throw new Error(`[skills] Skill ${path} must define frontmatter ${field} as a non-empty string.`);
113
+ }
114
+ return value.trim();
115
+ }
116
+ function optionalString(value) {
117
+ if (typeof value !== 'string')
118
+ return undefined;
119
+ const trimmed = value.trim();
120
+ return trimmed.length > 0 ? trimmed : undefined;
121
+ }
122
+ /** Agent Skills forbids reserved vendor words in a skill name. */
123
+ const RESERVED_NAME_WORDS = ['anthropic', 'claude'];
124
+ /** Frontmatter is injected into the system prompt, so markup in it is a prompt-injection seam. */
125
+ const XML_TAG_PATTERN = /<\/?[a-zA-Z][^>]*>/;
126
+ export function validateSkillName(name, path) {
127
+ if (name.length > 64) {
128
+ throw new Error(`[skills] Skill ${path} field "name" must be at most 64 characters.`);
129
+ }
130
+ if (XML_TAG_PATTERN.test(name)) {
131
+ throw new Error(`[skills] Skill ${path} field "name" must not contain XML tags.`);
132
+ }
133
+ if (!NAME_PATTERN.test(name)) {
134
+ throw new Error(`[skills] Skill ${path} field "name" "${name}" must match /^[a-z0-9]+(?:-[a-z0-9]+)*$/.`);
135
+ }
136
+ const reserved = RESERVED_NAME_WORDS.find((word) => name.includes(word));
137
+ if (reserved) {
138
+ throw new Error(`[skills] Skill ${path} field "name" must not contain the reserved word "${reserved}".`);
139
+ }
140
+ }
141
+ export function validateSkillDescription(description, path) {
142
+ if (codePointLength(description) > 1024) {
143
+ throw new Error(`[skills] Skill ${path} field "description" exceeds 1024 characters.`);
144
+ }
145
+ if (XML_TAG_PATTERN.test(description)) {
146
+ throw new Error(`[skills] Skill ${path} field "description" must not contain XML tags.`);
147
+ }
148
+ }
149
+ function codePointLength(value) {
150
+ return [...value].length;
151
+ }
152
+ function parseAllowedTools(value) {
153
+ if (value === undefined || value === null)
154
+ return undefined;
155
+ if (Array.isArray(value)) {
156
+ const tools = value.map((v) => String(v).trim()).filter(Boolean);
157
+ return tools.length > 0 ? tools : undefined;
158
+ }
159
+ if (typeof value === 'string') {
160
+ const trimmed = value.trim();
161
+ if (!trimmed)
162
+ return undefined;
163
+ const tools = trimmed.includes(',')
164
+ ? trimmed.split(',').map((s) => s.trim()).filter(Boolean)
165
+ : trimmed.split(/\s+/).filter(Boolean);
166
+ return tools.length > 0 ? tools : undefined;
167
+ }
168
+ return undefined;
169
+ }
170
+ function parseMetadata(value) {
171
+ if (value === undefined || value === null)
172
+ return undefined;
173
+ if (typeof value !== 'object' || Array.isArray(value))
174
+ return undefined;
175
+ const entries = Object.entries(value);
176
+ if (entries.length === 0)
177
+ return undefined;
178
+ const meta = {};
179
+ for (const [k, v] of entries) {
180
+ meta[k] = String(v);
181
+ }
182
+ return meta;
183
+ }
@@ -1,3 +1,4 @@
1
+ import type { FileSystem } from '../types/filesystem.js';
1
2
  import { type AnyTool } from '../types/effectTool.js';
2
3
  import type { PromptSection } from '../capabilities/index.js';
3
4
  import { type SkillWireAgent } from './collectSkills.js';
@@ -7,4 +8,4 @@ export interface WiredAgentSkills {
7
8
  tools: Record<string, AnyTool>;
8
9
  promptSections: PromptSection[];
9
10
  }
10
- export declare function wireAgentSkills(agent: SkillWireAgent): Promise<WiredAgentSkills | undefined>;
11
+ export declare function wireAgentSkills(agent: SkillWireAgent, fs?: FileSystem): Promise<WiredAgentSkills | undefined>;
@@ -1,10 +1,10 @@
1
1
  import { defineTool } from '../types/effectTool.js';
2
2
  import { collectRegisteredNames, prepareSkillStore, validateSkillAllowedTools, } from './collectSkills.js';
3
3
  import { SkillsCapability } from './SkillsCapability.js';
4
- export async function wireAgentSkills(agent) {
4
+ export async function wireAgentSkills(agent, fs) {
5
5
  if (!agent.skills)
6
6
  return undefined;
7
- const { store, skills } = await prepareSkillStore(agent.skills);
7
+ const { store, skills } = await prepareSkillStore(agent.skills, fs);
8
8
  validateSkillAllowedTools(skills, collectRegisteredNames(agent));
9
9
  const metas = await store.list();
10
10
  const capability = new SkillsCapability(store, metas);
@@ -1,4 +1,4 @@
1
- import type { HarnessStreamPart, TurnHandle } from '../types/stream.js';
1
+ import type { StreamPart, TurnHandle } from '../types/stream.js';
2
2
  import type { TurnResult } from '../types/channel.js';
3
3
  import type { Session } from '../types/session.js';
4
4
  import type { UserInputContent } from '../runtime/userInput.js';
@@ -13,8 +13,8 @@ export interface CreateMockRuntimeOptions {
13
13
  sessions?: Map<string, Session>;
14
14
  onRun?: (call: MockRuntimeRunCall) => void;
15
15
  }
16
- export declare function createMockTurnHandle(events: AsyncIterable<HarnessStreamPart>, settled?: TurnResult): TurnHandle;
16
+ export declare function createMockTurnHandle(events: AsyncIterable<StreamPart>, settled?: TurnResult): TurnHandle;
17
17
  export declare function createMockSession(partial?: Partial<Session>): Session;
18
- type MockRuntimeEvents = HarnessStreamPart[] | AsyncIterable<HarnessStreamPart> | (() => never);
18
+ type MockRuntimeEvents = StreamPart[] | AsyncIterable<StreamPart> | (() => never);
19
19
  export declare function createMockRuntime(parts: MockRuntimeEvents, options?: CreateMockRuntimeOptions): RuntimeLike;
20
20
  export {};