@kuralle-agents/core 0.13.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 (158) hide show
  1. package/README.md +4 -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 +1 -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/collectUntilComplete.js +4 -4
  11. package/dist/flow/degrade.js +8 -4
  12. package/dist/flow/emitInteractive.d.ts +2 -2
  13. package/dist/flow/emitInteractive.js +6 -3
  14. package/dist/flow/extraction.d.ts +2 -2
  15. package/dist/flow/extraction.js +10 -4
  16. package/dist/flow/reduceTransition.d.ts +2 -2
  17. package/dist/flow/reduceTransition.js +7 -3
  18. package/dist/flow/runFlow.js +34 -12
  19. package/dist/foundation/AgentStateController.d.ts +1 -1
  20. package/dist/foundation/ToolExecutor.d.ts +2 -2
  21. package/dist/foundation/index.d.ts +0 -11
  22. package/dist/foundation/index.js +0 -5
  23. package/dist/index.d.ts +9 -14
  24. package/dist/index.js +5 -6
  25. package/dist/outcomes/streamPart.d.ts +2 -2
  26. package/dist/outcomes/streamPart.js +2 -3
  27. package/dist/prompts/PromptBuilder.d.ts +3 -3
  28. package/dist/prompts/PromptBuilder.js +6 -6
  29. package/dist/prompts/index.d.ts +1 -1
  30. package/dist/prompts/types.d.ts +7 -2
  31. package/dist/runtime/InMemoryRetrievalCache.d.ts +1 -1
  32. package/dist/runtime/KnowledgeProvider.d.ts +3 -2
  33. package/dist/runtime/KnowledgeProvider.js +54 -30
  34. package/dist/runtime/Runtime.d.ts +3 -1
  35. package/dist/runtime/Runtime.js +84 -30
  36. package/dist/runtime/TraceRecorder.d.ts +22 -4
  37. package/dist/runtime/TraceRecorder.js +65 -29
  38. package/dist/runtime/buildAgentToolSurface.js +1 -1
  39. package/dist/runtime/channels/TextDriver.js +12 -9
  40. package/dist/runtime/channels/executeModelTool.d.ts +6 -0
  41. package/dist/runtime/channels/executeModelTool.js +70 -10
  42. package/dist/runtime/channels/extractionTurn.js +1 -1
  43. package/dist/runtime/channels/index.d.ts +0 -3
  44. package/dist/runtime/channels/index.js +0 -6
  45. package/dist/runtime/channels/streaming/hostControlSpeak.js +5 -1
  46. package/dist/runtime/channels/streaming/speakGated.js +24 -12
  47. package/dist/runtime/citations/index.d.ts +3 -3
  48. package/dist/runtime/closeRun.js +3 -2
  49. package/dist/runtime/controlFlowSignal.d.ts +24 -0
  50. package/dist/runtime/controlFlowSignal.js +29 -0
  51. package/dist/runtime/ctx.d.ts +3 -4
  52. package/dist/runtime/ctx.js +39 -5
  53. package/dist/runtime/durable/RunStore.js +1 -1
  54. package/dist/runtime/grounding/gather.d.ts +1 -1
  55. package/dist/runtime/grounding/inMemoryKnowledge.d.ts +1 -1
  56. package/dist/runtime/grounding/knowledge.d.ts +1 -1
  57. package/dist/runtime/hostLoop.js +26 -6
  58. package/dist/runtime/index.d.ts +1 -2
  59. package/dist/runtime/index.js +1 -1
  60. package/dist/runtime/outcomeMarking.d.ts +2 -2
  61. package/dist/runtime/outcomeMarking.js +2 -1
  62. package/dist/runtime/policies/agentTurn.d.ts +1 -1
  63. package/dist/runtime/runHookSafely.d.ts +3 -0
  64. package/dist/runtime/runHookSafely.js +19 -0
  65. package/dist/scheduler/index.d.ts +2 -2
  66. package/dist/scheduler/index.js +1 -1
  67. package/dist/services/TracingService.js +2 -3
  68. package/dist/skills/collectSkills.d.ts +11 -3
  69. package/dist/skills/collectSkills.js +51 -8
  70. package/dist/skills/compositeSkillStore.d.ts +26 -0
  71. package/dist/skills/compositeSkillStore.js +54 -0
  72. package/dist/skills/defineSkill.d.ts +21 -0
  73. package/dist/skills/defineSkill.js +26 -0
  74. package/dist/skills/fsSkillStore.d.ts +3 -0
  75. package/dist/skills/fsSkillStore.js +79 -0
  76. package/dist/skills/index.d.ts +4 -0
  77. package/dist/skills/index.js +4 -0
  78. package/dist/skills/parseSkillFrontmatter.d.ts +14 -0
  79. package/dist/skills/parseSkillFrontmatter.js +183 -0
  80. package/dist/skills/wireAgentSkills.d.ts +2 -1
  81. package/dist/skills/wireAgentSkills.js +2 -2
  82. package/dist/testing/mocks.d.ts +3 -3
  83. package/dist/testing/mocks.js +7 -3
  84. package/dist/tools/controlResults.d.ts +14 -0
  85. package/dist/tools/controlResults.js +15 -0
  86. package/dist/tools/effect/ToolExecutor.d.ts +6 -0
  87. package/dist/tools/effect/ToolExecutor.js +71 -35
  88. package/dist/tools/effect/defineTool.d.ts +9 -0
  89. package/dist/tools/effect/defineTool.js +1 -0
  90. package/dist/tools/effect/errors.d.ts +2 -5
  91. package/dist/tools/effect/errors.js +2 -5
  92. package/dist/tools/index.d.ts +0 -1
  93. package/dist/tools/index.js +0 -1
  94. package/dist/types/effectTool.d.ts +2 -0
  95. package/dist/types/flow.d.ts +1 -1
  96. package/dist/types/guardrails.d.ts +5 -0
  97. package/dist/types/hooks.d.ts +2 -2
  98. package/dist/types/index.d.ts +25 -16
  99. package/dist/types/index.js +8 -13
  100. package/dist/types/knowledge.d.ts +213 -0
  101. package/dist/types/processors.d.ts +0 -58
  102. package/dist/types/run-context.d.ts +3 -7
  103. package/dist/types/runtime.d.ts +3 -74
  104. package/dist/types/session.d.ts +1 -1
  105. package/dist/types/skills.d.ts +15 -1
  106. package/dist/types/stream.d.ts +133 -71
  107. package/dist/types/stream.js +34 -1
  108. package/dist/types/trace.d.ts +2 -0
  109. package/guides/AGENTS.md +3 -3
  110. package/guides/FLOWS.md +1 -1
  111. package/guides/GETTING_STARTED.md +1 -1
  112. package/guides/RUNTIME.md +20 -15
  113. package/guides/TOOLS.md +1 -1
  114. package/package.json +3 -13
  115. package/dist/foundation/AgentDefinition.d.ts +0 -17
  116. package/dist/foundation/ConversationEventLog.d.ts +0 -71
  117. package/dist/foundation/ConversationEventLog.js +0 -1
  118. package/dist/foundation/ConversationState.d.ts +0 -33
  119. package/dist/foundation/ConversationState.js +0 -1
  120. package/dist/foundation/DefaultConversationEventLog.d.ts +0 -27
  121. package/dist/foundation/DefaultConversationEventLog.js +0 -205
  122. package/dist/foundation/DefaultConversationState.d.ts +0 -36
  123. package/dist/foundation/DefaultConversationState.js +0 -103
  124. package/dist/foundation/DefaultToolExecutor.d.ts +0 -57
  125. package/dist/foundation/DefaultToolExecutor.js +0 -132
  126. package/dist/foundation/createFoundation.d.ts +0 -32
  127. package/dist/foundation/createFoundation.js +0 -33
  128. package/dist/hooks/HookRunner.d.ts +0 -37
  129. package/dist/hooks/HookRunner.js +0 -111
  130. package/dist/hooks/builtin/logging.d.ts +0 -5
  131. package/dist/hooks/builtin/logging.js +0 -98
  132. package/dist/hooks/builtin/metrics.d.ts +0 -7
  133. package/dist/hooks/builtin/metrics.js +0 -60
  134. package/dist/hooks/builtin/observability.d.ts +0 -20
  135. package/dist/hooks/builtin/observability.js +0 -572
  136. package/dist/hooks/helpers.d.ts +0 -22
  137. package/dist/hooks/helpers.js +0 -183
  138. package/dist/hooks/index.d.ts +0 -8
  139. package/dist/hooks/index.js +0 -6
  140. package/dist/realtime/RealtimeAudioClient.d.ts +0 -166
  141. package/dist/realtime/RealtimeAudioClient.js +0 -14
  142. package/dist/realtime/RealtimeSessionHandle.d.ts +0 -6
  143. package/dist/realtime/RealtimeSessionHandle.js +0 -1
  144. package/dist/realtime/index.d.ts +0 -2
  145. package/dist/realtime/index.js +0 -1
  146. package/dist/runtime/channels/VoiceDriver.d.ts +0 -31
  147. package/dist/runtime/channels/VoiceDriver.js +0 -321
  148. package/dist/runtime/channels/voiceTools.d.ts +0 -9
  149. package/dist/runtime/channels/voiceTools.js +0 -51
  150. package/dist/tools/errorHandling.d.ts +0 -35
  151. package/dist/tools/errorHandling.js +0 -145
  152. package/dist/types/voice.d.ts +0 -517
  153. package/dist/types/voice.js +0 -14
  154. package/dist/utils/isRecord.d.ts +0 -1
  155. package/dist/utils/isRecord.js +0 -3
  156. package/dist/utils/messageNormalization.d.ts +0 -2
  157. package/dist/utils/messageNormalization.js +0 -137
  158. /package/dist/{foundation/AgentDefinition.js → types/knowledge.js} +0 -0
@@ -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 {};
@@ -37,12 +37,16 @@ export function createMockSession(partial = {}) {
37
37
  handoffHistory: partial.handoffHistory ?? [],
38
38
  };
39
39
  }
40
+ function isAsyncIterable(value) {
41
+ return Symbol.asyncIterator in value;
42
+ }
40
43
  export function createMockRuntime(parts, options = {}) {
41
44
  const sessions = options.sessions ?? new Map();
42
45
  const run = (opts) => {
43
46
  if (typeof parts === 'function') {
44
47
  throw parts();
45
48
  }
49
+ const eventSource = parts;
46
50
  options.onRun?.({
47
51
  sessionId: opts.sessionId,
48
52
  input: opts.input,
@@ -50,13 +54,13 @@ export function createMockRuntime(parts, options = {}) {
50
54
  seedMessages: opts.seedMessages,
51
55
  });
52
56
  async function* events() {
53
- if (Symbol.asyncIterator in Object(parts)) {
54
- for await (const part of parts) {
57
+ if (isAsyncIterable(eventSource)) {
58
+ for await (const part of eventSource) {
55
59
  yield part;
56
60
  }
57
61
  return;
58
62
  }
59
- for (const part of parts) {
63
+ for (const part of eventSource) {
60
64
  yield part;
61
65
  }
62
66
  }
@@ -13,3 +13,17 @@ export declare function toolErrorResult(error: unknown): {
13
13
  error: true;
14
14
  message: string;
15
15
  };
16
+ export interface ToolDeniedResult {
17
+ __denied: true;
18
+ toolName: string;
19
+ deniedBy?: string;
20
+ message: string;
21
+ }
22
+ /**
23
+ * What the model sees when a human declines a `needsApproval` tool it asked to run.
24
+ *
25
+ * A result rather than an error, so the agent can tell the user the request was declined
26
+ * instead of the turn dying. `__denied` (not `error: true`) keeps it distinguishable from a
27
+ * genuine failure — nothing malfunctioned.
28
+ */
29
+ export declare function toolDeniedResult(toolName: string, deniedBy?: string): ToolDeniedResult;
@@ -16,3 +16,18 @@ export function toolErrorResult(error) {
16
16
  const message = error instanceof Error ? error.message : String(error);
17
17
  return { error: true, message };
18
18
  }
19
+ /**
20
+ * What the model sees when a human declines a `needsApproval` tool it asked to run.
21
+ *
22
+ * A result rather than an error, so the agent can tell the user the request was declined
23
+ * instead of the turn dying. `__denied` (not `error: true`) keeps it distinguishable from a
24
+ * genuine failure — nothing malfunctioned.
25
+ */
26
+ export function toolDeniedResult(toolName, deniedBy) {
27
+ return {
28
+ __denied: true,
29
+ toolName,
30
+ deniedBy,
31
+ message: `The "${toolName}" action was not approved${deniedBy ? ` by ${deniedBy}` : ''}. Tell the user it was declined; do not retry it.`,
32
+ };
33
+ }
@@ -9,6 +9,11 @@ export interface CoreToolExecutorConfig {
9
9
  parallelExecution?: boolean;
10
10
  agentId?: string;
11
11
  onInterim?: (message: string, toolName: string) => void;
12
+ /**
13
+ * Called for each chunk an async-iterable tool yields, as it arrives. The aggregate is
14
+ * still the tool's result; this is progress, not output.
15
+ */
16
+ onChunk?: (chunk: unknown, toolName: string, toolCallId: string) => void;
12
17
  }
13
18
  export interface CoreExecuteArgs {
14
19
  name: string;
@@ -26,6 +31,7 @@ export declare class CoreToolExecutor implements EffectToolExecutor {
26
31
  private readonly parallelExecution;
27
32
  private readonly agentId;
28
33
  private readonly onInterim?;
34
+ private readonly onChunk?;
29
35
  private readonly pairing;
30
36
  private executionGate;
31
37
  private callHistory;
@@ -3,12 +3,44 @@ import { debug } from '../../debug.js';
3
3
  import { cancelledPlaceholder, inProgressPlaceholder, PairingTracker, } from './pairing.js';
4
4
  import { ToolTimeoutError } from './errors.js';
5
5
  import { ToolValidationError, validateAndSanitize, validateOutput } from './schema.js';
6
+ import { isControlFlowSignal } from '../../runtime/controlFlowSignal.js';
7
+ /**
8
+ * Merges the caller's abort with a timeout so the tool sees one signal. `AbortSignal.any` is
9
+ * present on Node 20+, Bun, and workerd; the manual path keeps older runtimes working.
10
+ * Mirrors `composeSignals` in `@kuralle-agents/fs`.
11
+ */
12
+ function composeSignals(signal, timeoutSignal) {
13
+ if (!signal)
14
+ return timeoutSignal;
15
+ if (!timeoutSignal)
16
+ return signal;
17
+ if (typeof AbortSignal.any === 'function') {
18
+ return AbortSignal.any([signal, timeoutSignal]);
19
+ }
20
+ const controller = new AbortController();
21
+ const onAbort = () => controller.abort();
22
+ signal.addEventListener('abort', onAbort, { once: true });
23
+ timeoutSignal.addEventListener('abort', onAbort, { once: true });
24
+ if (signal.aborted || timeoutSignal.aborted)
25
+ onAbort();
26
+ return controller.signal;
27
+ }
28
+ function rejectOnAbort(signal, makeError) {
29
+ return new Promise((_, reject) => {
30
+ if (signal.aborted) {
31
+ reject(makeError());
32
+ return;
33
+ }
34
+ signal.addEventListener('abort', () => reject(makeError()), { once: true });
35
+ });
36
+ }
6
37
  export class CoreToolExecutor {
7
38
  tools;
8
39
  enforcer;
9
40
  parallelExecution;
10
41
  agentId;
11
42
  onInterim;
43
+ onChunk;
12
44
  pairing = new PairingTracker();
13
45
  executionGate = Promise.resolve();
14
46
  callHistory = [];
@@ -18,6 +50,7 @@ export class CoreToolExecutor {
18
50
  this.parallelExecution = config.parallelExecution ?? false;
19
51
  this.agentId = config.agentId ?? 'agent';
20
52
  this.onInterim = config.onInterim;
53
+ this.onChunk = config.onChunk;
21
54
  }
22
55
  getPairs() {
23
56
  return this.pairing.getAllPairs();
@@ -97,19 +130,14 @@ export class CoreToolExecutor {
97
130
  }
98
131
  }
99
132
  let interimTimer;
100
- let timeoutTimer;
101
- let interimSent = false;
102
133
  const onAbort = () => {
103
134
  if (interimTimer)
104
135
  clearTimeout(interimTimer);
105
- if (timeoutTimer)
106
- clearTimeout(timeoutTimer);
107
136
  };
108
137
  abortSignal?.addEventListener('abort', onAbort, { once: true });
109
138
  try {
110
139
  if (def.interim && def.interimAfterMs != null && def.interimAfterMs >= 0) {
111
140
  interimTimer = setTimeout(() => {
112
- interimSent = true;
113
141
  this.onInterim?.(def.interim, name);
114
142
  this.pairing.closePair(requestId, 'in_progress', inProgressPlaceholder(requestId, name, def.interim));
115
143
  }, def.interimAfterMs);
@@ -117,40 +145,36 @@ export class CoreToolExecutor {
117
145
  interimTimer.unref();
118
146
  }
119
147
  }
148
+ const timeoutMs = def.timeoutMs;
149
+ // The tool receives the timeout as an abort, so a cooperative tool stops working when
150
+ // we stop waiting for it. Racing alone abandoned the promise while the work continued.
151
+ const timeoutSignal = timeoutMs != null && timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : undefined;
152
+ const effectiveSignal = composeSignals(abortSignal, timeoutSignal);
120
153
  const executeCtx = toolCtx
121
- ? { ...toolCtx, abortSignal }
122
- : abortSignal
123
- ? { abortSignal }
154
+ ? { ...toolCtx, abortSignal: effectiveSignal }
155
+ : effectiveSignal
156
+ ? { abortSignal: effectiveSignal }
124
157
  : undefined;
125
158
  const executePromise = Promise.resolve(def.execute(sanitizedArgs, executeCtx)).then(async (result) => {
126
159
  if (result && typeof result[Symbol.asyncIterator] === 'function') {
127
160
  const chunks = [];
128
161
  for await (const chunk of result) {
129
162
  chunks.push(chunk);
163
+ // Surface progress as it arrives. Observational only — the journal records the
164
+ // aggregate below, so a replayed step emits nothing and stays deterministic.
165
+ this.onChunk?.(chunk, name, requestId);
130
166
  }
131
167
  return chunks.length === 1 ? chunks[0] : chunks;
132
168
  }
133
169
  return result;
134
170
  });
171
+ // The signals above cancel a cooperative tool; these racers stop us waiting on one that
172
+ // ignores them. Both are needed — neither alone bounds the call.
135
173
  const abortPromise = abortSignal && def.interruptible !== false
136
- ? new Promise((_, reject) => {
137
- if (abortSignal.aborted) {
138
- reject(new DOMException('Aborted', 'AbortError'));
139
- return;
140
- }
141
- abortSignal.addEventListener('abort', () => reject(new DOMException('Aborted', 'AbortError')), { once: true });
142
- })
174
+ ? rejectOnAbort(abortSignal, () => new DOMException('Aborted', 'AbortError'))
143
175
  : null;
144
- const timeoutMs = def.timeoutMs;
145
- const timeoutPromise = timeoutMs != null && timeoutMs > 0
146
- ? new Promise((_, reject) => {
147
- timeoutTimer = setTimeout(() => {
148
- reject(new ToolTimeoutError(name, timeoutMs));
149
- }, timeoutMs);
150
- if (typeof timeoutTimer === 'object' && 'unref' in timeoutTimer) {
151
- timeoutTimer.unref();
152
- }
153
- })
176
+ const timeoutPromise = timeoutSignal
177
+ ? rejectOnAbort(timeoutSignal, () => new ToolTimeoutError(name, timeoutMs))
154
178
  : null;
155
179
  const racers = [executePromise];
156
180
  if (abortPromise)
@@ -160,25 +184,16 @@ export class CoreToolExecutor {
160
184
  const rawResult = racers.length > 1 ? await Promise.race(racers) : await executePromise;
161
185
  if (interimTimer)
162
186
  clearTimeout(interimTimer);
163
- if (timeoutTimer)
164
- clearTimeout(timeoutTimer);
165
187
  const validated = await validateOutput(def.output, rawResult, name);
166
188
  callRecord.result = validated;
167
189
  callRecord.durationMs = Date.now() - callRecord.timestamp;
168
190
  this.callHistory.push(callRecord);
169
- if (!interimSent) {
170
- this.pairing.closePair(requestId, 'completed', validated);
171
- }
172
- else {
173
- this.pairing.closePair(requestId, 'completed', validated);
174
- }
191
+ this.pairing.closePair(requestId, 'completed', validated);
175
192
  return validated;
176
193
  }
177
194
  catch (error) {
178
195
  if (interimTimer)
179
196
  clearTimeout(interimTimer);
180
- if (timeoutTimer)
181
- clearTimeout(timeoutTimer);
182
197
  if (error instanceof DOMException && error.name === 'AbortError') {
183
198
  const placeholder = cancelledPlaceholder(requestId, name);
184
199
  callRecord.success = false;
@@ -192,6 +207,27 @@ export class CoreToolExecutor {
192
207
  this.pairing.closePair(requestId, 'validation_failed', undefined, error.message);
193
208
  throw error;
194
209
  }
210
+ if (error instanceof ToolTimeoutError || isControlFlowSignal(error)) {
211
+ // A timeout and a suspend are decisions about the run, not results the tool may
212
+ // reinterpret. They reach the caller unchanged.
213
+ throw error;
214
+ }
215
+ if (def.onError) {
216
+ // Recovery runs before the failure is recorded, so a handled error is journaled as
217
+ // the success it became. A throwing handler falls through to the generic path below.
218
+ try {
219
+ const recovered = await def.onError(error instanceof Error ? error : new Error(String(error)), sanitizedArgs);
220
+ const validatedRecovery = await validateOutput(def.output, recovered, name);
221
+ callRecord.result = validatedRecovery;
222
+ callRecord.durationMs = Date.now() - callRecord.timestamp;
223
+ this.callHistory.push(callRecord);
224
+ this.pairing.closePair(requestId, 'completed', validatedRecovery);
225
+ return validatedRecovery;
226
+ }
227
+ catch (recoveryError) {
228
+ error = recoveryError;
229
+ }
230
+ }
195
231
  const err = error instanceof Error ? error : new Error(String(error));
196
232
  callRecord.success = false;
197
233
  callRecord.error = err;
@@ -22,6 +22,15 @@ export declare function defineTool<S extends z.ZodTypeAny | StandardSchemaV1 | u
22
22
  parallelSafe?: boolean;
23
23
  idempotencyKey?: (args: InferToolInput<S>) => string;
24
24
  execute: (args: InferToolInput<S>, ctx?: ToolContext) => Promise<R> | AsyncIterable<R>;
25
+ /**
26
+ * Turns a thrown error into a result the model can act on, instead of a generic failure.
27
+ * Return a value to recover; rethrow (or omit this) to let the error propagate.
28
+ *
29
+ * Runs only for genuine failures — never for a timeout, an abort, an input/output schema
30
+ * violation, or a control-flow signal, all of which must stay distinguishable from a
31
+ * result the tool chose to return.
32
+ */
33
+ onError?: (error: Error, args: InferToolInput<S>) => Promise<R> | R;
25
34
  }): Tool<InferToolInput<S>, R>;
26
35
  export declare function toolToAiSdk<TInput = unknown, TOutput = unknown>(def: Tool<TInput, TOutput>): AiTool<TInput, TOutput>;
27
36
  export declare function buildToolSet(tools: Record<string, AnyTool>): ToolSet;
@@ -14,6 +14,7 @@ export function defineTool(config) {
14
14
  parallelSafe: config.parallelSafe,
15
15
  idempotencyKey: config.idempotencyKey,
16
16
  execute: config.execute,
17
+ onError: config.onError,
17
18
  };
18
19
  }
19
20
  export function toolToAiSdk(def) {
@@ -1,13 +1,10 @@
1
- /**
2
- * Thrown by `ctx.tool(...)` when a tool declared `needsApproval: true` is denied
3
- * by a human (the `__approval` signal resolves with `approved: false`). Catch it
4
- * inside the calling flow `action` node to route gracefully (e.g. escalate or end).
5
- */
1
+ /** Thrown when a tool execution exceeds its configured timeout. */
6
2
  export declare class ToolTimeoutError extends Error {
7
3
  readonly toolName: string;
8
4
  readonly timeoutMs: number;
9
5
  constructor(toolName: string, timeoutMs: number);
10
6
  }
7
+ /** Thrown when a human denies a tool call that requires approval. */
11
8
  export declare class ToolApprovalDeniedError extends Error {
12
9
  readonly toolName: string;
13
10
  readonly by?: string;