@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.
- package/README.md +4 -4
- package/dist/ai-sdk/uiMessageStream.d.ts +2 -2
- package/dist/ai-sdk/uiMessageStream.js +42 -29
- package/dist/capabilities/LivePromptAssembler.d.ts +1 -1
- package/dist/capabilities/LivePromptAssembler.js +1 -1
- package/dist/capabilities/index.d.ts +1 -1
- package/dist/eval/EvalRunner.js +4 -4
- package/dist/eval/simulation.js +4 -3
- package/dist/events/TurnHandle.d.ts +3 -3
- package/dist/flow/collectUntilComplete.js +4 -4
- package/dist/flow/degrade.js +8 -4
- package/dist/flow/emitInteractive.d.ts +2 -2
- package/dist/flow/emitInteractive.js +6 -3
- package/dist/flow/extraction.d.ts +2 -2
- package/dist/flow/extraction.js +10 -4
- package/dist/flow/reduceTransition.d.ts +2 -2
- package/dist/flow/reduceTransition.js +7 -3
- package/dist/flow/runFlow.js +34 -12
- package/dist/foundation/AgentStateController.d.ts +1 -1
- package/dist/foundation/ToolExecutor.d.ts +2 -2
- package/dist/foundation/index.d.ts +0 -11
- package/dist/foundation/index.js +0 -5
- package/dist/index.d.ts +9 -14
- package/dist/index.js +5 -6
- package/dist/outcomes/streamPart.d.ts +2 -2
- package/dist/outcomes/streamPart.js +2 -3
- package/dist/prompts/PromptBuilder.d.ts +3 -3
- package/dist/prompts/PromptBuilder.js +6 -6
- package/dist/prompts/index.d.ts +1 -1
- package/dist/prompts/types.d.ts +7 -2
- package/dist/runtime/InMemoryRetrievalCache.d.ts +1 -1
- package/dist/runtime/KnowledgeProvider.d.ts +3 -2
- package/dist/runtime/KnowledgeProvider.js +54 -30
- package/dist/runtime/Runtime.d.ts +3 -1
- package/dist/runtime/Runtime.js +84 -30
- package/dist/runtime/TraceRecorder.d.ts +22 -4
- package/dist/runtime/TraceRecorder.js +65 -29
- package/dist/runtime/buildAgentToolSurface.js +1 -1
- package/dist/runtime/channels/TextDriver.js +12 -9
- package/dist/runtime/channels/executeModelTool.d.ts +6 -0
- package/dist/runtime/channels/executeModelTool.js +70 -10
- package/dist/runtime/channels/extractionTurn.js +1 -1
- package/dist/runtime/channels/index.d.ts +0 -3
- package/dist/runtime/channels/index.js +0 -6
- package/dist/runtime/channels/streaming/hostControlSpeak.js +5 -1
- package/dist/runtime/channels/streaming/speakGated.js +24 -12
- package/dist/runtime/citations/index.d.ts +3 -3
- package/dist/runtime/closeRun.js +3 -2
- package/dist/runtime/controlFlowSignal.d.ts +24 -0
- package/dist/runtime/controlFlowSignal.js +29 -0
- package/dist/runtime/ctx.d.ts +3 -4
- package/dist/runtime/ctx.js +39 -5
- package/dist/runtime/durable/RunStore.js +1 -1
- package/dist/runtime/grounding/gather.d.ts +1 -1
- package/dist/runtime/grounding/inMemoryKnowledge.d.ts +1 -1
- package/dist/runtime/grounding/knowledge.d.ts +1 -1
- package/dist/runtime/hostLoop.js +26 -6
- package/dist/runtime/index.d.ts +1 -2
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/outcomeMarking.d.ts +2 -2
- package/dist/runtime/outcomeMarking.js +2 -1
- package/dist/runtime/policies/agentTurn.d.ts +1 -1
- package/dist/runtime/runHookSafely.d.ts +3 -0
- package/dist/runtime/runHookSafely.js +19 -0
- package/dist/scheduler/index.d.ts +2 -2
- package/dist/scheduler/index.js +1 -1
- package/dist/services/TracingService.js +2 -3
- package/dist/skills/collectSkills.d.ts +11 -3
- package/dist/skills/collectSkills.js +51 -8
- package/dist/skills/compositeSkillStore.d.ts +26 -0
- package/dist/skills/compositeSkillStore.js +54 -0
- package/dist/skills/defineSkill.d.ts +21 -0
- package/dist/skills/defineSkill.js +26 -0
- package/dist/skills/fsSkillStore.d.ts +3 -0
- package/dist/skills/fsSkillStore.js +79 -0
- package/dist/skills/index.d.ts +4 -0
- package/dist/skills/index.js +4 -0
- package/dist/skills/parseSkillFrontmatter.d.ts +14 -0
- package/dist/skills/parseSkillFrontmatter.js +183 -0
- package/dist/skills/wireAgentSkills.d.ts +2 -1
- package/dist/skills/wireAgentSkills.js +2 -2
- package/dist/testing/mocks.d.ts +3 -3
- package/dist/testing/mocks.js +7 -3
- package/dist/tools/controlResults.d.ts +14 -0
- package/dist/tools/controlResults.js +15 -0
- package/dist/tools/effect/ToolExecutor.d.ts +6 -0
- package/dist/tools/effect/ToolExecutor.js +71 -35
- package/dist/tools/effect/defineTool.d.ts +9 -0
- package/dist/tools/effect/defineTool.js +1 -0
- package/dist/tools/effect/errors.d.ts +2 -5
- package/dist/tools/effect/errors.js +2 -5
- package/dist/tools/index.d.ts +0 -1
- package/dist/tools/index.js +0 -1
- package/dist/types/effectTool.d.ts +2 -0
- package/dist/types/flow.d.ts +1 -1
- package/dist/types/guardrails.d.ts +5 -0
- package/dist/types/hooks.d.ts +2 -2
- package/dist/types/index.d.ts +25 -16
- package/dist/types/index.js +8 -13
- package/dist/types/knowledge.d.ts +213 -0
- package/dist/types/processors.d.ts +0 -58
- package/dist/types/run-context.d.ts +3 -7
- package/dist/types/runtime.d.ts +3 -74
- package/dist/types/session.d.ts +1 -1
- package/dist/types/skills.d.ts +15 -1
- package/dist/types/stream.d.ts +133 -71
- package/dist/types/stream.js +34 -1
- package/dist/types/trace.d.ts +2 -0
- package/guides/AGENTS.md +3 -3
- package/guides/FLOWS.md +1 -1
- package/guides/GETTING_STARTED.md +1 -1
- package/guides/RUNTIME.md +20 -15
- package/guides/TOOLS.md +1 -1
- package/package.json +3 -13
- package/dist/foundation/AgentDefinition.d.ts +0 -17
- package/dist/foundation/ConversationEventLog.d.ts +0 -71
- package/dist/foundation/ConversationEventLog.js +0 -1
- package/dist/foundation/ConversationState.d.ts +0 -33
- package/dist/foundation/ConversationState.js +0 -1
- package/dist/foundation/DefaultConversationEventLog.d.ts +0 -27
- package/dist/foundation/DefaultConversationEventLog.js +0 -205
- package/dist/foundation/DefaultConversationState.d.ts +0 -36
- package/dist/foundation/DefaultConversationState.js +0 -103
- package/dist/foundation/DefaultToolExecutor.d.ts +0 -57
- package/dist/foundation/DefaultToolExecutor.js +0 -132
- package/dist/foundation/createFoundation.d.ts +0 -32
- package/dist/foundation/createFoundation.js +0 -33
- package/dist/hooks/HookRunner.d.ts +0 -37
- package/dist/hooks/HookRunner.js +0 -111
- package/dist/hooks/builtin/logging.d.ts +0 -5
- package/dist/hooks/builtin/logging.js +0 -98
- package/dist/hooks/builtin/metrics.d.ts +0 -7
- package/dist/hooks/builtin/metrics.js +0 -60
- package/dist/hooks/builtin/observability.d.ts +0 -20
- package/dist/hooks/builtin/observability.js +0 -572
- package/dist/hooks/helpers.d.ts +0 -22
- package/dist/hooks/helpers.js +0 -183
- package/dist/hooks/index.d.ts +0 -8
- package/dist/hooks/index.js +0 -6
- package/dist/realtime/RealtimeAudioClient.d.ts +0 -166
- package/dist/realtime/RealtimeAudioClient.js +0 -14
- package/dist/realtime/RealtimeSessionHandle.d.ts +0 -6
- package/dist/realtime/RealtimeSessionHandle.js +0 -1
- package/dist/realtime/index.d.ts +0 -2
- package/dist/realtime/index.js +0 -1
- package/dist/runtime/channels/VoiceDriver.d.ts +0 -31
- package/dist/runtime/channels/VoiceDriver.js +0 -321
- package/dist/runtime/channels/voiceTools.d.ts +0 -9
- package/dist/runtime/channels/voiceTools.js +0 -51
- package/dist/tools/errorHandling.d.ts +0 -35
- package/dist/tools/errorHandling.js +0 -145
- package/dist/types/voice.d.ts +0 -517
- package/dist/types/voice.js +0 -14
- package/dist/utils/isRecord.d.ts +0 -1
- package/dist/utils/isRecord.js +0 -3
- package/dist/utils/messageNormalization.d.ts +0 -2
- package/dist/utils/messageNormalization.js +0 -137
- /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
|
+
}
|
package/dist/skills/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/skills/index.js
CHANGED
|
@@ -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);
|
package/dist/testing/mocks.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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<
|
|
16
|
+
export declare function createMockTurnHandle(events: AsyncIterable<StreamPart>, settled?: TurnResult): TurnHandle;
|
|
17
17
|
export declare function createMockSession(partial?: Partial<Session>): Session;
|
|
18
|
-
type MockRuntimeEvents =
|
|
18
|
+
type MockRuntimeEvents = StreamPart[] | AsyncIterable<StreamPart> | (() => never);
|
|
19
19
|
export declare function createMockRuntime(parts: MockRuntimeEvents, options?: CreateMockRuntimeOptions): RuntimeLike;
|
|
20
20
|
export {};
|
package/dist/testing/mocks.js
CHANGED
|
@@ -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 (
|
|
54
|
-
for await (const part of
|
|
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
|
|
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
|
-
:
|
|
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
|
-
?
|
|
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
|
|
145
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -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;
|