feat: sync shared skills contract and graph-first discipline
This commit is contained in:
@@ -7,7 +7,7 @@ import { validateEdgeExplanation } from '@/services/database/quality';
|
||||
|
||||
export const createEdgeTool = tool({
|
||||
description:
|
||||
'Create a relationship between two nodes only after the user has explicitly confirmed the proposed connection. Use this as the execution step after you surfaced candidate edges in plain language and got a clear yes. Provide an explanation and the system will infer the type and direction.\n\n' +
|
||||
'Create a relationship between two nodes only after the user has explicitly confirmed the proposed connection. Check for an existing relationship first, then use this as the execution step after you surfaced candidate edges in plain language and got a clear yes. Provide an explanation and the system will infer the type and direction.\n\n' +
|
||||
'Examples of explanations:\n' +
|
||||
'- "Written by" (book → author)\n' +
|
||||
'- "Episode of this podcast" (episode → podcast)\n' +
|
||||
|
||||
@@ -57,7 +57,7 @@ function inferSourceFromContext(params: { title: string; description?: string; s
|
||||
}
|
||||
|
||||
export const createNodeTool = tool({
|
||||
description: 'Create a node after you have already decided this should be a net-new write. If the user explicitly asked to save or import something and duplicate/update checks are complete, write immediately. If you are only suggesting a save, propose the node first and wait for confirmation. Focus on a clean title, a strong natural description that says what the thing is, preserved source text, and the right metadata. When the node comes from the user\'s own idea, note, or dictated thought, preserve their actual wording in source with only minimal cleanup instead of flattening it into a summary. Do not block creation if the description is incomplete. If the description framing is materially inferred, create the node first and then invite one concise user correction pass.',
|
||||
description: 'Create a node after you have already decided this should be a net-new write. Search first when practical, and prefer updateNode if the artifact is clearly the same thing and a new node would be redundant. If the user explicitly asked to save or import something and duplicate/update checks are complete, write immediately. If you are only suggesting a save, propose the node first and wait for confirmation. Focus on a clean title, a strong natural description that says what the thing is, preserved source text, and the right metadata. When the node comes from the user\'s own idea, note, or dictated thought, preserve their actual wording in source with only minimal cleanup instead of flattening it into a summary. Do not block creation if the description is incomplete. If the description framing is materially inferred, create the node first and then invite one concise user correction pass.',
|
||||
inputSchema: z.object({
|
||||
title: z.string().describe('The title of the node'),
|
||||
description: z.string().max(500).optional().describe('Optional natural description. If you have enough context, describe what this is, why it belongs in Brad\'s graph, and its current workflow status in normal prose. Do not use labels like WHAT:, WHY:, or STATUS:.'),
|
||||
|
||||
@@ -3,12 +3,13 @@ import { z } from 'zod';
|
||||
import { nodeService } from '@/services/database/nodes';
|
||||
|
||||
export const getNodesByIdTool = tool({
|
||||
description: 'Load full node records by IDs',
|
||||
description: 'Load full node records by IDs. Use this before rewriting an existing node source when the focused-node excerpt is insufficient, because it returns the current description, source text, metadata, and timestamps needed for disciplined updates.',
|
||||
inputSchema: z.object({
|
||||
nodeIds: z.array(z.number().int().positive()).min(1).max(10).describe('List of node IDs to load'),
|
||||
includeSourcePreview: z.boolean().default(true).describe('Whether to return a trimmed source preview for each node'),
|
||||
includeSource: z.boolean().default(true).describe('Whether to return source text for each node'),
|
||||
sourceCharLimit: z.number().int().min(200).max(20000).default(10000).describe('Max source characters per node before truncation metadata is added'),
|
||||
}),
|
||||
execute: async ({ nodeIds, includeSourcePreview }) => {
|
||||
execute: async ({ nodeIds, includeSource, sourceCharLimit }) => {
|
||||
const uniqueIds = Array.from(new Set(nodeIds.filter(id => Number.isFinite(id) && id > 0)));
|
||||
if (uniqueIds.length === 0) {
|
||||
return {
|
||||
@@ -23,23 +24,25 @@ export const getNodesByIdTool = tool({
|
||||
try {
|
||||
const node = await nodeService.getNodeById(id);
|
||||
if (!node) return null;
|
||||
const preview = includeSourcePreview
|
||||
? (node.source || node.description || '')
|
||||
.split(/\s+/)
|
||||
.slice(0, 80)
|
||||
.join(' ')
|
||||
.trim()
|
||||
: undefined;
|
||||
const rawSource = typeof node.source === 'string' ? node.source : '';
|
||||
const source = includeSource
|
||||
? rawSource.slice(0, sourceCharLimit).trim() || null
|
||||
: null;
|
||||
const sourceLength = rawSource.length;
|
||||
const sourceTruncated = includeSource ? sourceLength > sourceCharLimit : false;
|
||||
|
||||
return {
|
||||
id: node.id,
|
||||
title: node.title,
|
||||
description: node.description ?? null,
|
||||
source,
|
||||
source_length: sourceLength,
|
||||
source_truncated: sourceTruncated,
|
||||
link: node.link,
|
||||
event_date: node.event_date ?? null,
|
||||
chunk_status: node.chunk_status || 'unknown',
|
||||
created_at: node.created_at,
|
||||
updated_at: node.updated_at,
|
||||
source_preview: preview || null,
|
||||
metadata: node.metadata ?? null,
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { edgeService } from '@/services/database/edges';
|
||||
import { validateEdgeExplanation } from '@/services/database/quality';
|
||||
|
||||
export const updateEdgeTool = tool({
|
||||
description: 'Update an edge explanation and/or source only after the user explicitly confirmed the corrected relationship. Explanations must explicitly state the relationship.',
|
||||
description: 'Update an existing edge only after the user explicitly confirmed the corrected relationship. Use this when the connection already exists and only the explanation or classification needs to change. Explanations must explicitly state why the relationship exists.',
|
||||
inputSchema: z.object({
|
||||
confirmed_by_user: z.boolean().describe('Must be true. Reject the edge update otherwise.'),
|
||||
edge_id: z.number().describe('The ID of the edge to update'),
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getInternalApiBaseUrl } from '@/services/runtime/apiBase';
|
||||
import { getInternalAuthHeaders } from '@/services/auth/internalAuth';
|
||||
|
||||
export const updateNodeTool = tool({
|
||||
description: 'Update node fields when the existing node is clearly the same artifact and a net-new node would be redundant. Explicit user-directed updates should proceed once the target node is clear; if you are only proposing a change, ask first. Use this to enrich or correct nodes without losing canonical source content. When fixing a user-authored idea node, source should preserve the user\'s original wording as fully as possible. Never block an update because the description is incomplete. If the new description framing is materially inferred, complete the update and then invite one concise user feedback pass.',
|
||||
description: 'Update node fields when the existing node is clearly the same artifact and a net-new node would be redundant. Explicit user-directed updates should proceed once the target node is clear; if you are only proposing a change, ask first. Use this to enrich or correct nodes without losing canonical source content. When changing source on an existing node, inspect the current source first via the focused-node excerpt or getNodesById, then include source_update_basis as a short exact excerpt you inspected. When fixing a user-authored idea node, source should preserve the user\'s original wording as fully as possible. Never block an update because the description is incomplete. If the new description framing is materially inferred, complete the update and then invite one concise user feedback pass.',
|
||||
inputSchema: z.object({
|
||||
id: z.number().describe('The ID of the node to update'),
|
||||
updates: z.object({
|
||||
@@ -14,9 +14,10 @@ export const updateNodeTool = tool({
|
||||
link: z.string().optional().describe('New link'),
|
||||
event_date: z.string().optional().describe('When the thing actually happened (ISO 8601). Not when it was added to the graph.'),
|
||||
metadata: z.record(z.any()).optional().describe('Metadata patch. It now merges with existing metadata instead of replacing the full blob. Use canonical keys: type, state, captured_method, captured_by, source_metadata.')
|
||||
}).passthrough().describe('Object containing the fields to update. Derived analysis should be stored in a separate linked node, not appended to the source node.')
|
||||
}).passthrough().describe('Object containing the fields to update. Derived analysis should be stored in a separate linked node, not appended to the source node.'),
|
||||
source_update_basis: z.string().optional().describe('When updating source on a node that already has source text, include a short exact excerpt from the current source you inspected first. This is required for source rewrites unless the existing source is empty.')
|
||||
}),
|
||||
execute: async ({ id, updates }) => {
|
||||
execute: async ({ id, updates, source_update_basis }) => {
|
||||
try {
|
||||
if (!updates || Object.keys(updates).length === 0) {
|
||||
return {
|
||||
@@ -30,7 +31,7 @@ export const updateNodeTool = tool({
|
||||
const response = await fetch(`${getInternalApiBaseUrl()}/api/nodes/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: getInternalAuthHeaders({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify(updates)
|
||||
body: JSON.stringify({ ...updates, source_update_basis })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { listGuides } from '@/services/guides/guideService';
|
||||
|
||||
export const listGuidesTool = tool({
|
||||
description: 'List all available guides with their names and descriptions.',
|
||||
inputSchema: z.object({}),
|
||||
execute: async () => {
|
||||
try {
|
||||
const guides = listGuides();
|
||||
return {
|
||||
success: true,
|
||||
data: guides,
|
||||
message: `Found ${guides.length} guides`,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[listGuides] error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to list guides',
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { readGuide } from '@/services/guides/guideService';
|
||||
|
||||
export const readGuideTool = tool({
|
||||
description: 'Read a guide by name. Returns the full markdown content with instructions.',
|
||||
inputSchema: z.object({
|
||||
name: z.string().describe('The name of the guide to read'),
|
||||
}),
|
||||
execute: async ({ name }) => {
|
||||
try {
|
||||
const guide = readGuide(name);
|
||||
if (!guide) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Guide "${name}" not found`,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
data: guide,
|
||||
message: `Loaded guide: ${guide.name}`,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[readGuide] error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to read guide',
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { writeGuide } from '@/services/guides/guideService';
|
||||
import { eventBroadcaster } from '@/services/events';
|
||||
|
||||
export const writeGuideTool = tool({
|
||||
description: 'Write or update a guide. Content should be full markdown with YAML frontmatter (name, description).',
|
||||
inputSchema: z.object({
|
||||
name: z.string().describe('The name of the guide to write'),
|
||||
content: z.string().describe('Full markdown content including YAML frontmatter'),
|
||||
}),
|
||||
execute: async ({ name, content }) => {
|
||||
try {
|
||||
writeGuide(name, content);
|
||||
eventBroadcaster.broadcast({ type: 'GUIDE_UPDATED', data: { name } });
|
||||
return {
|
||||
success: true,
|
||||
data: { name },
|
||||
message: `Guide "${name}" saved`,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[writeGuide] error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to write guide',
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -19,7 +19,7 @@ export const deleteSkillTool = tool({
|
||||
};
|
||||
}
|
||||
|
||||
eventBroadcaster.broadcast({ type: 'GUIDE_UPDATED', data: { name } });
|
||||
eventBroadcaster.broadcast({ type: 'SKILL_UPDATED', data: { name } });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -27,6 +27,7 @@ export const deleteSkillTool = tool({
|
||||
message: `Skill "${name}" deleted`,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[deleteSkill] error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to delete skill',
|
||||
|
||||
@@ -20,7 +20,7 @@ export const writeSkillTool = tool({
|
||||
};
|
||||
}
|
||||
|
||||
eventBroadcaster.broadcast({ type: 'GUIDE_UPDATED', data: { name } });
|
||||
eventBroadcaster.broadcast({ type: 'SKILL_UPDATED', data: { name } });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -28,6 +28,7 @@ export const writeSkillTool = tool({
|
||||
message: `Skill "${name}" saved`,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[writeSkill] error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to write skill',
|
||||
|
||||
Reference in New Issue
Block a user