@kuralle-agents/core 0.5.0 → 0.6.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 (64) hide show
  1. package/README.md +1 -1
  2. package/dist/flow/nodeBuilders.d.ts +1 -1
  3. package/dist/flow/nodeBuilders.js +5 -3
  4. package/dist/index.d.ts +12 -4
  5. package/dist/index.js +7 -2
  6. package/dist/memory/blocks/FilePersistentMemoryStore.d.ts +1 -1
  7. package/dist/memory/blocks/FilePersistentMemoryStore.js +9 -0
  8. package/dist/memory/blocks/InMemoryPersistentMemoryStore.d.ts +8 -0
  9. package/dist/memory/blocks/InMemoryPersistentMemoryStore.js +25 -0
  10. package/dist/memory/blocks/RoutedPersistentMemoryStore.d.ts +18 -0
  11. package/dist/memory/blocks/RoutedPersistentMemoryStore.js +35 -0
  12. package/dist/memory/blocks/TieredPersistentMemoryStore.d.ts +10 -0
  13. package/dist/memory/blocks/TieredPersistentMemoryStore.js +30 -0
  14. package/dist/memory/blocks/memoryBlockTool.d.ts +5 -5
  15. package/dist/memory/blocks/testing.d.ts +11 -0
  16. package/dist/memory/blocks/testing.js +59 -0
  17. package/dist/memory/index.d.ts +4 -0
  18. package/dist/memory/index.js +3 -0
  19. package/dist/runtime/Runtime.d.ts +3 -0
  20. package/dist/runtime/Runtime.js +47 -5
  21. package/dist/runtime/agentReply.js +2 -1
  22. package/dist/runtime/channels/TextDriver.js +3 -2
  23. package/dist/runtime/channels/VoiceDriver.js +3 -3
  24. package/dist/runtime/channels/extractionTurn.js +1 -1
  25. package/dist/runtime/ctx.d.ts +2 -0
  26. package/dist/runtime/ctx.js +1 -0
  27. package/dist/runtime/grounding/defaultStoreRegistry.d.ts +3 -0
  28. package/dist/runtime/grounding/defaultStoreRegistry.js +10 -0
  29. package/dist/runtime/grounding/index.d.ts +1 -0
  30. package/dist/runtime/grounding/index.js +1 -0
  31. package/dist/runtime/grounding/workingMemory.d.ts +19 -0
  32. package/dist/runtime/grounding/workingMemory.js +80 -0
  33. package/dist/runtime/resolveAgentWorkspace.d.ts +10 -0
  34. package/dist/runtime/resolveAgentWorkspace.js +9 -0
  35. package/dist/skills/SkillsCapability.d.ts +10 -0
  36. package/dist/skills/SkillsCapability.js +52 -0
  37. package/dist/skills/collectSkills.d.ts +17 -0
  38. package/dist/skills/collectSkills.js +56 -0
  39. package/dist/skills/index.d.ts +5 -0
  40. package/dist/skills/index.js +4 -0
  41. package/dist/skills/inlineSkillStore.d.ts +9 -0
  42. package/dist/skills/inlineSkillStore.js +37 -0
  43. package/dist/skills/wireAgentSkills.d.ts +10 -0
  44. package/dist/skills/wireAgentSkills.js +25 -0
  45. package/dist/tools/effect/defineTool.js +1 -1
  46. package/dist/tools/effect/index.d.ts +1 -0
  47. package/dist/tools/effect/index.js +1 -0
  48. package/dist/tools/effect/wrapAiSdkTool.d.ts +3 -0
  49. package/dist/tools/effect/wrapAiSdkTool.js +12 -0
  50. package/dist/tools/fs/createFsTool.d.ts +30 -0
  51. package/dist/tools/fs/createFsTool.js +200 -0
  52. package/dist/types/agentConfig.d.ts +16 -3
  53. package/dist/types/filesystem.d.ts +85 -0
  54. package/dist/types/filesystem.js +6 -0
  55. package/dist/types/grounding.d.ts +12 -0
  56. package/dist/types/index.d.ts +2 -0
  57. package/dist/types/index.js +2 -0
  58. package/dist/types/run-context.d.ts +11 -2
  59. package/dist/types/skills.d.ts +19 -0
  60. package/dist/types/skills.js +1 -0
  61. package/guides/AGENTS.md +2 -3
  62. package/guides/FLOWS.md +2 -2
  63. package/guides/TOOLS.md +69 -2
  64. package/package.json +12 -3
@@ -0,0 +1,10 @@
1
+ let nodeDefaultStoreFactory;
2
+ export function registerNodeDefaultWorkingMemoryStore(factory) {
3
+ nodeDefaultStoreFactory = factory;
4
+ }
5
+ export function getNodeDefaultWorkingMemoryStore() {
6
+ if (typeof process === 'undefined' || !process.versions?.node) {
7
+ return undefined;
8
+ }
9
+ return nodeDefaultStoreFactory;
10
+ }
@@ -1,5 +1,6 @@
1
1
  export { appendGatherBlocks, buildAutoRetrieveProvider, buildKnowledgeProvider, } from './knowledge.js';
2
2
  export { buildMemoryService, resetMissingUserIdWarningsForTests, runMemoryIngest, warnMissingUserId, } from './memory.js';
3
+ export { wireWorkingMemory, loadWorkingMemoryBlocks, formatWorkingMemorySection, resolveWorkingMemoryStore, resolveWorkingMemoryOwner, type LoadedWorkingMemoryBlock, type WiredWorkingMemory, } from './workingMemory.js';
3
4
  export { runGatherPhase, type GatherResult, type GatherScope } from './gather.js';
4
5
  export { resolveNodeGatherScope } from './nodeScope.js';
5
6
  export { createInMemoryKnowledgeConfig, createInMemoryKnowledgeRetriever, type InMemoryKnowledgeDocument, } from './inMemoryKnowledge.js';
@@ -1,5 +1,6 @@
1
1
  export { appendGatherBlocks, buildAutoRetrieveProvider, buildKnowledgeProvider, } from './knowledge.js';
2
2
  export { buildMemoryService, resetMissingUserIdWarningsForTests, runMemoryIngest, warnMissingUserId, } from './memory.js';
3
+ export { wireWorkingMemory, loadWorkingMemoryBlocks, formatWorkingMemorySection, resolveWorkingMemoryStore, resolveWorkingMemoryOwner, } from './workingMemory.js';
3
4
  export { runGatherPhase } from './gather.js';
4
5
  export { resolveNodeGatherScope } from './nodeScope.js';
5
6
  export { createInMemoryKnowledgeConfig, createInMemoryKnowledgeRetriever, } from './inMemoryKnowledge.js';
@@ -0,0 +1,19 @@
1
+ import type { AgentConfig } from '../../types/agentConfig.js';
2
+ import type { WorkingMemoryBlockSpec, WorkingMemoryConfig } from '../../types/grounding.js';
3
+ import type { Session } from '../../types/session.js';
4
+ import type { AnyTool } from '../../types/effectTool.js';
5
+ import { type MemoryBlockScope, type PersistentMemoryStore } from '../../memory/blocks/types.js';
6
+ export interface LoadedWorkingMemoryBlock {
7
+ scope: MemoryBlockScope;
8
+ key: string;
9
+ content: string;
10
+ }
11
+ export interface WiredWorkingMemory {
12
+ promptSection: string | undefined;
13
+ memoryBlockTool: AnyTool;
14
+ }
15
+ export declare function resolveWorkingMemoryStore(config: WorkingMemoryConfig, harnessDefault?: PersistentMemoryStore): PersistentMemoryStore;
16
+ export declare function resolveWorkingMemoryOwner(scope: MemoryBlockScope, agentId: string, userId: string | undefined): string;
17
+ export declare function loadWorkingMemoryBlocks(store: PersistentMemoryStore, autoLoad: WorkingMemoryBlockSpec[], resolveOwner: (scope: MemoryBlockScope) => string): Promise<LoadedWorkingMemoryBlock[]>;
18
+ export declare function formatWorkingMemorySection(blocks: LoadedWorkingMemoryBlock[], autoLoad: WorkingMemoryBlockSpec[]): string | undefined;
19
+ export declare function wireWorkingMemory(agent: AgentConfig, session: Session, harnessDefaultStore?: PersistentMemoryStore): Promise<WiredWorkingMemory | undefined>;
@@ -0,0 +1,80 @@
1
+ import { buildMemoryBlockTool } from '../../memory/blocks/memoryBlockTool.js';
2
+ import { DEFAULT_AUTO_LOAD_BLOCKS, DEFAULT_BLOCK_CHAR_LIMIT, } from '../../memory/blocks/types.js';
3
+ import { wrapAiSdkTool } from '../../tools/effect/wrapAiSdkTool.js';
4
+ import { getNodeDefaultWorkingMemoryStore } from './defaultStoreRegistry.js';
5
+ export function resolveWorkingMemoryStore(config, harnessDefault) {
6
+ if (config.store) {
7
+ return config.store;
8
+ }
9
+ if (harnessDefault) {
10
+ return harnessDefault;
11
+ }
12
+ const factory = getNodeDefaultWorkingMemoryStore();
13
+ if (factory) {
14
+ return factory();
15
+ }
16
+ throw new Error('[Kuralle] agent.memory.workingMemory requires a store. Pass workingMemory.store, ' +
17
+ 'HarnessConfig.defaultWorkingMemoryStore, or import FilePersistentMemoryStore on Node.');
18
+ }
19
+ export function resolveWorkingMemoryOwner(scope, agentId, userId) {
20
+ return scope === 'agent' ? agentId : (userId ?? 'anonymous');
21
+ }
22
+ export async function loadWorkingMemoryBlocks(store, autoLoad, resolveOwner) {
23
+ const loaded = [];
24
+ for (const spec of autoLoad) {
25
+ const owner = resolveOwner(spec.scope);
26
+ const block = await store.loadBlock(spec.scope, owner, spec.key);
27
+ let content = block?.content?.trim() ?? '';
28
+ if (!content && spec.template) {
29
+ content = spec.template.trim();
30
+ }
31
+ if (content) {
32
+ loaded.push({ scope: spec.scope, key: spec.key, content });
33
+ }
34
+ }
35
+ return loaded;
36
+ }
37
+ export function formatWorkingMemorySection(blocks, autoLoad) {
38
+ if (autoLoad.length === 0) {
39
+ return undefined;
40
+ }
41
+ const byKey = new Map(blocks.map((b) => [`${b.scope}/${b.key}`, b]));
42
+ // Directive (Mastra-informed): the model must proactively maintain these blocks
43
+ // via the `memory_block` tool, not just read them. Rendered even when blocks are
44
+ // empty so a first-time conversation knows the capability exists.
45
+ const lines = [
46
+ '## Working memory',
47
+ '',
48
+ 'You keep durable notes about the user and conversation in the blocks below, persisted across sessions. Use the `memory_block` tool to keep them current:',
49
+ '- When the user shares a durable fact or preference (name, account details, preferences, anything that may be referenced again), call `memory_block` with action `add`, the relevant block, and a short factual entry. Store proactively — if unsure whether it will matter later, store it.',
50
+ '- Answer questions about stored information from these blocks first; you do NOT need to call the tool to read them.',
51
+ '- Do not announce that you are saving, and do not call the tool when nothing relevant changed.',
52
+ '',
53
+ ];
54
+ for (const spec of autoLoad) {
55
+ const block = byKey.get(`${spec.scope}/${spec.key}`);
56
+ lines.push(`### ${spec.key} (${spec.scope})`);
57
+ lines.push(block?.content?.trim() || '(empty — add entries here as you learn them)');
58
+ lines.push('');
59
+ }
60
+ return lines.join('\n').trim();
61
+ }
62
+ export async function wireWorkingMemory(agent, session, harnessDefaultStore) {
63
+ const config = agent.memory?.workingMemory;
64
+ if (!config) {
65
+ return undefined;
66
+ }
67
+ const store = resolveWorkingMemoryStore(config, harnessDefaultStore);
68
+ const autoLoad = config.autoLoad ?? DEFAULT_AUTO_LOAD_BLOCKS;
69
+ const charLimit = config.defaultCharLimit ?? DEFAULT_BLOCK_CHAR_LIMIT;
70
+ const resolveOwner = (scope) => resolveWorkingMemoryOwner(scope, agent.id, session.userId);
71
+ const loaded = await loadWorkingMemoryBlocks(store, autoLoad, resolveOwner);
72
+ const promptSection = formatWorkingMemorySection(loaded, autoLoad);
73
+ const memoryBlockTool = wrapAiSdkTool('memory_block', buildMemoryBlockTool({
74
+ store,
75
+ resolveOwner,
76
+ charLimit,
77
+ scanForInjection: config.scanForInjection,
78
+ }));
79
+ return { promptSection, memoryBlockTool };
80
+ }
@@ -0,0 +1,10 @@
1
+ import type { FileSystem } from '../types/filesystem.js';
2
+ export type AgentWorkspaceConfig = FileSystem | {
3
+ fs: FileSystem;
4
+ readOnly?: boolean;
5
+ };
6
+ export interface ResolvedAgentWorkspace {
7
+ fs: FileSystem;
8
+ readOnly: boolean;
9
+ }
10
+ export declare function resolveAgentWorkspace(workspace: AgentWorkspaceConfig | undefined): ResolvedAgentWorkspace | undefined;
@@ -0,0 +1,9 @@
1
+ export function resolveAgentWorkspace(workspace) {
2
+ if (!workspace) {
3
+ return undefined;
4
+ }
5
+ if (typeof workspace === 'object' && workspace !== null && 'fs' in workspace) {
6
+ return { fs: workspace.fs, readOnly: workspace.readOnly !== false };
7
+ }
8
+ return { fs: workspace, readOnly: true };
9
+ }
@@ -0,0 +1,10 @@
1
+ import type { Capability, CapabilityAction, PromptSection, ToolDeclaration } from '../capabilities/index.js';
2
+ import type { SkillMeta, SkillStoreLike } from '../types/skills.js';
3
+ export declare class SkillsCapability implements Capability {
4
+ private readonly store;
5
+ private readonly metas;
6
+ constructor(store: SkillStoreLike, metas: SkillMeta[]);
7
+ getTools(): ToolDeclaration[];
8
+ getPromptSections(): PromptSection[];
9
+ processToolResult(_toolName: string, _args: unknown, _result: unknown): CapabilityAction | null;
10
+ }
@@ -0,0 +1,52 @@
1
+ import { z } from 'zod';
2
+ export class SkillsCapability {
3
+ store;
4
+ metas;
5
+ constructor(store, metas) {
6
+ this.store = store;
7
+ this.metas = metas;
8
+ }
9
+ getTools() {
10
+ return [
11
+ {
12
+ name: 'load_skill',
13
+ description: "Load a skill's full instructions by name when the task matches its description.",
14
+ parameters: z.object({
15
+ name: z.string().describe('Skill name from the available skills list'),
16
+ }),
17
+ execute: async (args) => ({
18
+ body: await this.store.loadBody(args.name),
19
+ }),
20
+ },
21
+ {
22
+ name: 'read_skill_resource',
23
+ description: 'Read a bundled resource file from a skill (reference docs, checklists, etc.).',
24
+ parameters: z.object({
25
+ name: z.string().describe('Skill name'),
26
+ path: z.string().describe('Relative resource path within the skill folder'),
27
+ }),
28
+ execute: async (args) => ({
29
+ content: await this.store.loadResource(args.name, args.path),
30
+ }),
31
+ },
32
+ ];
33
+ }
34
+ getPromptSections() {
35
+ if (!this.metas.length)
36
+ return [];
37
+ const lines = this.metas.map((m) => `- ${m.name}: ${m.description}`).join('\n');
38
+ return [
39
+ {
40
+ role: 'context',
41
+ content: [
42
+ '## Available skills',
43
+ 'Load a skill with load_skill when its description matches the task:',
44
+ lines,
45
+ ].join('\n'),
46
+ },
47
+ ];
48
+ }
49
+ processToolResult(_toolName, _args, _result) {
50
+ return null;
51
+ }
52
+ }
@@ -0,0 +1,17 @@
1
+ import type { AnyTool } from '../types/effectTool.js';
2
+ import type { SkillLike, SkillSource, SkillStoreLike } from '../types/skills.js';
3
+ export interface SkillWireAgent {
4
+ skills?: SkillSource;
5
+ tools?: Record<string, AnyTool>;
6
+ globalTools?: Record<string, AnyTool>;
7
+ flows?: Array<{
8
+ name: string;
9
+ }>;
10
+ }
11
+ export declare function isSkillStore(value: SkillSource): value is SkillStoreLike;
12
+ export declare function collectRegisteredNames(agent: SkillWireAgent): Set<string>;
13
+ export declare function validateSkillAllowedTools(skills: SkillLike[], registered: Set<string>): void;
14
+ export declare function prepareSkillStore(source: SkillSource): Promise<{
15
+ store: SkillStoreLike;
16
+ skills: SkillLike[];
17
+ }>;
@@ -0,0 +1,56 @@
1
+ import { InlineSkillStore } from './inlineSkillStore.js';
2
+ export function isSkillStore(value) {
3
+ return (typeof value === 'object' &&
4
+ value !== null &&
5
+ !Array.isArray(value) &&
6
+ 'list' in value &&
7
+ typeof value.list === 'function');
8
+ }
9
+ async function collectSkillsFromSource(source) {
10
+ if (!isSkillStore(source)) {
11
+ return Array.isArray(source) ? source : [source];
12
+ }
13
+ if (typeof source.getAllSkills === 'function') {
14
+ const all = source.getAllSkills();
15
+ return Array.isArray(all) ? all : await all;
16
+ }
17
+ if (typeof source.loadAllSkills === 'function') {
18
+ return source.loadAllSkills();
19
+ }
20
+ const metas = await source.list();
21
+ const skills = [];
22
+ for (const meta of metas) {
23
+ const body = await source.loadBody(meta.name);
24
+ skills.push({ name: meta.name, description: meta.description, body });
25
+ }
26
+ return skills;
27
+ }
28
+ export function collectRegisteredNames(agent) {
29
+ const names = new Set();
30
+ for (const [key, tool] of Object.entries(agent.tools ?? {})) {
31
+ names.add(tool.name ?? key);
32
+ }
33
+ for (const [key, tool] of Object.entries(agent.globalTools ?? {})) {
34
+ names.add(tool.name ?? key);
35
+ }
36
+ for (const flow of agent.flows ?? []) {
37
+ names.add(flow.name);
38
+ }
39
+ return names;
40
+ }
41
+ export function validateSkillAllowedTools(skills, registered) {
42
+ for (const skill of skills) {
43
+ for (const toolName of skill.allowedTools ?? []) {
44
+ if (!registered.has(toolName)) {
45
+ throw new Error(`skill ${skill.name}: unknown tool ${toolName}`);
46
+ }
47
+ }
48
+ }
49
+ }
50
+ export async function prepareSkillStore(source) {
51
+ if (isSkillStore(source)) {
52
+ return { store: source, skills: await collectSkillsFromSource(source) };
53
+ }
54
+ const skills = Array.isArray(source) ? source : [source];
55
+ return { store: new InlineSkillStore(skills), skills };
56
+ }
@@ -0,0 +1,5 @@
1
+ export { SkillsCapability } from './SkillsCapability.js';
2
+ export { wireAgentSkills } from './wireAgentSkills.js';
3
+ export type { WiredAgentSkills } from './wireAgentSkills.js';
4
+ export { collectRegisteredNames, validateSkillAllowedTools, prepareSkillStore, isSkillStore, type SkillWireAgent, } from './collectSkills.js';
5
+ export { InlineSkillStore } from './inlineSkillStore.js';
@@ -0,0 +1,4 @@
1
+ export { SkillsCapability } from './SkillsCapability.js';
2
+ export { wireAgentSkills } from './wireAgentSkills.js';
3
+ export { collectRegisteredNames, validateSkillAllowedTools, prepareSkillStore, isSkillStore, } from './collectSkills.js';
4
+ export { InlineSkillStore } from './inlineSkillStore.js';
@@ -0,0 +1,9 @@
1
+ import type { SkillLike, SkillMeta, SkillStoreLike } from '../types/skills.js';
2
+ export declare class InlineSkillStore implements SkillStoreLike {
3
+ private readonly byName;
4
+ constructor(skills: SkillLike[]);
5
+ list(): Promise<SkillMeta[]>;
6
+ loadBody(name: string): Promise<string>;
7
+ loadResource(name: string, path: string): Promise<string | Uint8Array>;
8
+ getAllSkills(): SkillLike[];
9
+ }
@@ -0,0 +1,37 @@
1
+ export class InlineSkillStore {
2
+ byName;
3
+ constructor(skills) {
4
+ this.byName = new Map(skills.map((s) => [s.name, s]));
5
+ }
6
+ async list() {
7
+ return [...this.byName.values()].map((s) => ({
8
+ name: s.name,
9
+ description: s.description,
10
+ }));
11
+ }
12
+ async loadBody(name) {
13
+ const skill = this.byName.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 = this.byName.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 content = skill.resources?.[normalized];
29
+ if (content === undefined) {
30
+ throw new Error(`[skills] Resource "${normalized}" not found for skill "${name}".`);
31
+ }
32
+ return content;
33
+ }
34
+ getAllSkills() {
35
+ return [...this.byName.values()];
36
+ }
37
+ }
@@ -0,0 +1,10 @@
1
+ import { type AnyTool } from '../types/effectTool.js';
2
+ import type { PromptSection } from '../capabilities/index.js';
3
+ import { type SkillWireAgent } from './collectSkills.js';
4
+ import { SkillsCapability } from './SkillsCapability.js';
5
+ export interface WiredAgentSkills {
6
+ capability: SkillsCapability;
7
+ tools: Record<string, AnyTool>;
8
+ promptSections: PromptSection[];
9
+ }
10
+ export declare function wireAgentSkills(agent: SkillWireAgent): Promise<WiredAgentSkills | undefined>;
@@ -0,0 +1,25 @@
1
+ import { defineTool } from '../types/effectTool.js';
2
+ import { collectRegisteredNames, prepareSkillStore, validateSkillAllowedTools, } from './collectSkills.js';
3
+ import { SkillsCapability } from './SkillsCapability.js';
4
+ export async function wireAgentSkills(agent) {
5
+ if (!agent.skills)
6
+ return undefined;
7
+ const { store, skills } = await prepareSkillStore(agent.skills);
8
+ validateSkillAllowedTools(skills, collectRegisteredNames(agent));
9
+ const metas = await store.list();
10
+ const capability = new SkillsCapability(store, metas);
11
+ const tools = {};
12
+ for (const decl of capability.getTools()) {
13
+ tools[decl.name] = defineTool({
14
+ name: decl.name,
15
+ description: decl.description,
16
+ input: decl.parameters,
17
+ execute: async (args) => decl.execute(args),
18
+ });
19
+ }
20
+ return {
21
+ capability,
22
+ tools,
23
+ promptSections: capability.getPromptSections(),
24
+ };
25
+ }
@@ -25,7 +25,7 @@ export function toolToAiSdk(def) {
25
25
  // `buildToolSet` produces a model-facing ToolSet whose entries are schema-only
26
26
  // (`toolToAiSdk` strips `execute`). Stash the raw effect tools (with executors),
27
27
  // keyed by the returned ToolSet, so a flow node can recover its executors for
28
- // in-flow execution without separately registering them on `agent.effectTools`
28
+ // in-flow execution without separately registering them on `agent.tools`.
29
29
  // (see `resolveReplyNode`). The WeakMap is GC-friendly and invisible to callers.
30
30
  const rawToolsBySet = new WeakMap();
31
31
  export function buildToolSet(tools) {
@@ -1,4 +1,5 @@
1
1
  export { defineTool, toolToAiSdk, buildToolSet } from './defineTool.js';
2
+ export { wrapAiSdkTool } from './wrapAiSdkTool.js';
2
3
  export { CoreToolExecutor } from './ToolExecutor.js';
3
4
  export type { CoreToolExecutorConfig, CoreExecuteArgs } from './ToolExecutor.js';
4
5
  export { PairingTracker, cancelledPlaceholder, inProgressPlaceholder, } from './pairing.js';
@@ -1,4 +1,5 @@
1
1
  export { defineTool, toolToAiSdk, buildToolSet } from './defineTool.js';
2
+ export { wrapAiSdkTool } from './wrapAiSdkTool.js';
2
3
  export { CoreToolExecutor } from './ToolExecutor.js';
3
4
  export { PairingTracker, cancelledPlaceholder, inProgressPlaceholder, } from './pairing.js';
4
5
  export { validateAndSanitize, validateOutput, ToolValidationError } from './schema.js';
@@ -0,0 +1,3 @@
1
+ import type { Tool as AiTool } from 'ai';
2
+ import type { AnyTool } from '../../types/effectTool.js';
3
+ export declare function wrapAiSdkTool(name: string, aiTool: AiTool): AnyTool;
@@ -0,0 +1,12 @@
1
+ export function wrapAiSdkTool(name, aiTool) {
2
+ const exec = aiTool.execute;
3
+ if (typeof exec !== 'function') {
4
+ throw new Error(`wrapAiSdkTool("${name}"): AI SDK tool has no execute; use defineTool for schema-only tools.`);
5
+ }
6
+ return {
7
+ name,
8
+ description: aiTool.description ?? name,
9
+ input: aiTool.inputSchema,
10
+ execute: (args, ctx) => exec(args, ctx),
11
+ };
12
+ }
@@ -0,0 +1,30 @@
1
+ import type { AnyTool } from '../../types/effectTool.js';
2
+ import { type FileSystem } from '../../types/filesystem.js';
3
+ export interface CreateFsToolOptions {
4
+ fs: FileSystem;
5
+ readOnly?: boolean;
6
+ timeoutMs?: number;
7
+ }
8
+ export interface GrepHit {
9
+ path: string;
10
+ line: number;
11
+ text: string;
12
+ }
13
+ export interface FsSearchHit {
14
+ slug: string;
15
+ chunkIndex: number;
16
+ text: string;
17
+ }
18
+ export interface FsWithSearch extends FileSystem {
19
+ search?(pattern: string, opts?: {
20
+ limit?: number;
21
+ path?: string;
22
+ }): Promise<FsSearchHit[]>;
23
+ }
24
+ /**
25
+ * One durable `workspace` tool over a {@link FileSystem} (ls/cat/grep/find/read/write/edit).
26
+ * Lives in core (not `@kuralle-agents/fs`) because it needs only `defineTool` + the
27
+ * `FileSystem` interface — both core-owned — so the runtime can auto-register it with a
28
+ * static import and no core->fs dependency (RFC-02 §5.2). `@kuralle-agents/fs` re-exports it.
29
+ */
30
+ export declare function createFsTool(opts: CreateFsToolOptions): AnyTool;
@@ -0,0 +1,200 @@
1
+ import { z } from 'zod';
2
+ import { defineTool } from '../effect/defineTool.js';
3
+ import { fsErrorCode } from '../../types/filesystem.js';
4
+ const DEFAULT_READ_ONLY = true;
5
+ const workspaceInput = z.object({
6
+ op: z.enum(['ls', 'cat', 'grep', 'find', 'read', 'write', 'edit']),
7
+ path: z.string().optional(),
8
+ pattern: z.string().optional(),
9
+ root: z.string().optional(),
10
+ glob: z.string().optional(),
11
+ flags: z.string().optional(),
12
+ content: z.string().optional(),
13
+ find: z.string().optional(),
14
+ replace: z.string().optional(),
15
+ });
16
+ function normalizeFsPath(path, fallback = '/') {
17
+ if (!path || path.trim() === '')
18
+ return fallback;
19
+ return path;
20
+ }
21
+ function requireField(args, field, op) {
22
+ const value = args[field];
23
+ if (typeof value !== 'string' || value.length === 0) {
24
+ throw new Error(`EINVAL: missing ${String(field)} for workspace op '${op}'`);
25
+ }
26
+ return value;
27
+ }
28
+ function eroFs(path) {
29
+ return new Error(`EROFS: read-only filesystem, write '${path}'`);
30
+ }
31
+ function assertWritable(readOnly, path) {
32
+ if (readOnly)
33
+ throw eroFs(path);
34
+ }
35
+ async function listFiles(fs, root) {
36
+ const normalized = root === '' ? '/' : root;
37
+ const out = [];
38
+ const stack = [normalized];
39
+ while (stack.length > 0) {
40
+ const dir = stack.pop();
41
+ if (!(await fs.exists(dir))) {
42
+ throw new Error(`ENOENT: no such file or directory, find '${dir}'`);
43
+ }
44
+ const stat = await fs.stat(dir);
45
+ if (stat.type !== 'directory') {
46
+ out.push(dir);
47
+ continue;
48
+ }
49
+ const entries = await fs.readdirWithFileTypes(dir);
50
+ for (const entry of entries) {
51
+ const child = fs.resolvePath(dir, entry.name);
52
+ if (entry.type === 'directory') {
53
+ stack.push(child);
54
+ }
55
+ else {
56
+ out.push(child);
57
+ }
58
+ }
59
+ }
60
+ return out.sort();
61
+ }
62
+ async function grepFiles(fs, pattern, root, flags) {
63
+ let re;
64
+ try {
65
+ re = new RegExp(pattern, flags?.includes('i') ? 'i' : undefined);
66
+ }
67
+ catch {
68
+ throw new Error(`EINVAL: invalid grep pattern '${pattern}'`);
69
+ }
70
+ const searchable = fs;
71
+ if (searchable.search) {
72
+ const coarse = await searchable.search(pattern, { limit: 500, path: root });
73
+ const slugs = [...new Set(coarse.map((hit) => hit.slug))];
74
+ const hits = [];
75
+ for (const filePath of slugs) {
76
+ let content;
77
+ try {
78
+ content = await fs.readFile(filePath);
79
+ }
80
+ catch (err) {
81
+ if (fsErrorCode(err) === 'EISDIR')
82
+ continue;
83
+ throw err;
84
+ }
85
+ const lines = content.split('\n');
86
+ for (let i = 0; i < lines.length; i++) {
87
+ if (re.test(lines[i])) {
88
+ hits.push({ path: filePath, line: i + 1, text: lines[i] });
89
+ }
90
+ }
91
+ }
92
+ return hits;
93
+ }
94
+ const candidates = await listFiles(fs, root);
95
+ const hits = [];
96
+ for (const filePath of candidates) {
97
+ let content;
98
+ try {
99
+ content = await fs.readFile(filePath);
100
+ }
101
+ catch (err) {
102
+ if (fsErrorCode(err) === 'EISDIR')
103
+ continue;
104
+ throw err;
105
+ }
106
+ const lines = content.split('\n');
107
+ for (let i = 0; i < lines.length; i++) {
108
+ if (re.test(lines[i])) {
109
+ hits.push({ path: filePath, line: i + 1, text: lines[i] });
110
+ }
111
+ }
112
+ }
113
+ return hits;
114
+ }
115
+ /**
116
+ * One durable `workspace` tool over a {@link FileSystem} (ls/cat/grep/find/read/write/edit).
117
+ * Lives in core (not `@kuralle-agents/fs`) because it needs only `defineTool` + the
118
+ * `FileSystem` interface — both core-owned — so the runtime can auto-register it with a
119
+ * static import and no core->fs dependency (RFC-02 §5.2). `@kuralle-agents/fs` re-exports it.
120
+ */
121
+ export function createFsTool(opts) {
122
+ const { fs, readOnly = DEFAULT_READ_ONLY, timeoutMs } = opts;
123
+ return defineTool({
124
+ name: 'workspace',
125
+ description: 'Explore and edit the agent workspace. Ops: ls, cat, grep, find, read, write, edit.',
126
+ timeoutMs,
127
+ input: workspaceInput,
128
+ execute: async (args) => {
129
+ switch (args.op) {
130
+ case 'ls': {
131
+ const path = normalizeFsPath(args.path);
132
+ const entries = await fs.readdirWithFileTypes(path);
133
+ return {
134
+ op: args.op,
135
+ ok: true,
136
+ path,
137
+ entries,
138
+ };
139
+ }
140
+ case 'cat':
141
+ case 'read': {
142
+ const path = requireField(args, 'path', args.op);
143
+ const content = await fs.readFile(path);
144
+ return { op: args.op, ok: true, path, content };
145
+ }
146
+ case 'find': {
147
+ const root = normalizeFsPath(args.root);
148
+ const glob = requireField(args, 'glob', args.op);
149
+ const paths = await fs.glob(glob);
150
+ const rooted = paths.filter((p) => {
151
+ const normalizedRoot = root === '/' ? '/' : root.replace(/\/$/, '');
152
+ return p === normalizedRoot || p.startsWith(`${normalizedRoot}/`);
153
+ });
154
+ return {
155
+ op: args.op,
156
+ ok: true,
157
+ root,
158
+ glob,
159
+ paths: rooted,
160
+ };
161
+ }
162
+ case 'grep': {
163
+ const pattern = requireField(args, 'pattern', args.op);
164
+ const path = normalizeFsPath(args.path);
165
+ const hits = await grepFiles(fs, pattern, path, args.flags);
166
+ return {
167
+ op: args.op,
168
+ ok: true,
169
+ pattern,
170
+ path,
171
+ hits,
172
+ };
173
+ }
174
+ case 'write': {
175
+ const path = requireField(args, 'path', args.op);
176
+ const content = requireField(args, 'content', args.op);
177
+ assertWritable(readOnly, path);
178
+ await fs.writeFile(path, content);
179
+ return { op: args.op, ok: true, path };
180
+ }
181
+ case 'edit': {
182
+ const path = requireField(args, 'path', args.op);
183
+ const find = requireField(args, 'find', args.op);
184
+ const replace = requireField(args, 'replace', args.op);
185
+ assertWritable(readOnly, path);
186
+ const current = await fs.readFile(path);
187
+ if (!current.includes(find)) {
188
+ throw new Error(`ENOENT: find string not found in file, edit '${path}'`);
189
+ }
190
+ const next = current.replace(find, replace);
191
+ await fs.writeFile(path, next);
192
+ return { op: args.op, ok: true, path };
193
+ }
194
+ default: {
195
+ throw new Error(`EINVAL: unknown workspace op '${String(args.op)}'`);
196
+ }
197
+ }
198
+ },
199
+ });
200
+ }