Skip to content

Commit 8a97b3f

Browse files
authored
Merge pull request #74 from atomly/feat/ai-agent-create-page-tool
feat: Create-Page Tool & Improved System Prompt
2 parents eabe780 + a4d4962 commit 8a97b3f

21 files changed

+481
-104
lines changed

apps/web/src/ai/agents/journl-agent-context.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
1+
type JournalEntryEditor = {
2+
date: string;
3+
type: "journal-entry";
4+
};
5+
6+
type PageEditor = {
7+
id: string;
8+
title: string;
9+
type: "page";
10+
};
11+
112
/**
213
* The context of the Journl agent.
314
*/
415
export type JournlAgentContext = {
5-
activeEditors: string[];
16+
activeEditors: (JournalEntryEditor | PageEditor)[];
617
currentDate: string;
718
highlightedText: string[];
819
user: {

apps/web/src/ai/agents/journl-agent.ts

Lines changed: 61 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { Agent } from "@mastra/core/agent";
22
import { RuntimeContext } from "@mastra/core/runtime-context";
33
import { z } from "zod/v4";
44
import { model } from "~/ai/providers/openai/text";
5+
import { env } from "~/env";
6+
import { createPage } from "../tools/create-page";
57
import { manipulateEditor } from "../tools/manipulate-editor";
68
import { navigateJournalEntry } from "../tools/navigate-journal-entry";
79
import { navigatePage } from "../tools/navigate-page";
@@ -16,17 +18,53 @@ export const journlAgent = new Agent({
1618
description: `${AGENT_NAME}, an AI companion for personal reflection, journaling, and knowledge discovery.`,
1719
instructions: ({ runtimeContext }) => {
1820
const context = getJournlRuntimeContext(runtimeContext);
21+
if (env.NODE_ENV === "development") {
22+
console.debug("Journl context", context);
23+
}
1924
return `You are ${AGENT_NAME}, an AI companion that helps users write, navigate, and manage their own notes.
2025
2126
Current date: ${context.currentDate}
27+
User's name: ${context.user.name}
2228
2329
Do not reproduce song lyrics or any other copyrighted material, even if asked.
2430
31+
# User State (deterministic, read-only)
32+
33+
${
34+
context.view.name === "journal-timeline"
35+
? `- Currently at the journal timeline ${context.view.focusedDate ? `and engaged with the entry of the date ${context.view.focusedDate}` : ""}.`
36+
: context.view.name === "journal-entry"
37+
? `- Currently at the journal entry of date ${context.view.date}.`
38+
: context.view.name === "page"
39+
? `- Currently at the page of UUID ${context.view.id} with the title ${context.view.title}.`
40+
: "- Currently at a different view without editors."
41+
}
42+
${
43+
context.activeEditors.length > 0
44+
? `- ${context.activeEditors.length > 1 ? `There are ${context.activeEditors.length} active editors` : "There is one active editor"}: ${context.activeEditors.map((editor) => JSON.stringify(editor)).join(", ")}`
45+
: ""
46+
}
47+
${
48+
context.highlightedText.length > 0
49+
? `- User has highlighted text: ${context.highlightedText.join(", ")}.`
50+
: ""
51+
}
52+
2553
# Tools
2654
2755
### \`manipulateEditor\`
2856
29-
Modify the active editor (insert/append/prepend/replace text; headings, bullets, and so on). When you use the \`manipulateEditor\` tool, it immediately modifies the target editor in the UI. There is no background work; changes apply now.
57+
Modify the content of the target editor (insert/append/prepend/replace text; headings, bullets, and so on).
58+
59+
**Important**: The target editor has to be the ID of one of the active editors, if you don't know which to use, do not call this tool and ask the user to clarify instead.
60+
61+
- The generated \`userPrompt\` for the \`manipulateEditor\` tool MUST include as much detail as possible.
62+
- The prompt will be used by a different agent that will be manipulating the editor client-side, and should be treated as such.
63+
- Any content you generate should be markdown that is immediately usable. Avoid placeholder text.
64+
- Do not add titles to the pages because they are handled separately from the editor.
65+
- **No fabrication**. Never invent prior notes, pages, links, or other content.
66+
67+
After a successful call to the \`manipulateEditor\` tool, avoid telling the user that the changes were made. At most, summarize the changes in a few words.
3068
3169
Use when:
3270
- The user wants to write/add/insert/capture/log/note content.
@@ -36,13 +74,6 @@ Use when:
3674
Do not use when:
3775
- The user only wants recall/analysis of prior content (use search tools instead).
3876
39-
The generated \`userPrompt\` for the \`manipulateEditor\` tool MUST include as much detail as possible:
40-
41-
- **Content** — markdown that is immediately usable (headings, bullets/numbered lists, block quotes \`>\`, code fences). Avoid placeholder text.
42-
- **Voice** — preserve the user's phrasing for reflections; tighten only for structure
43-
- **No fabrication** — never invent prior notes or links
44-
- When producing checklists: 1) use \`- [ ]\` / \`- [x]\`; 2) one task per line; 3) keep tasks short and actionable.
45-
4677
### \`semanticJournalSearch\`
4778
4879
Semantic search over journal entries (daily notes). The user says or implies "find when I talked about X / patterns in Y / times I felt Z" or requests to search for a specific topic/theme/emotion.
@@ -69,57 +100,25 @@ Open a specific page by **UUID only**. The user says or implies "open/go to <pag
69100
70101
If you don't know the UUID of the page, use the \`semanticPageSearch\` tool to find it before using this tool.
71102
72-
# Examples
73-
74-
- “write/add/insert/capture/log/note”: \`manipulateEditor\`
75-
- “format/make a checklist/quote/code/heading/tag” \`manipulateEditor\`
76-
- “open/go to today/yesterday/2025-06-02/last Monday”: \`navigateJournalEntry\`
77-
- “open/go to <page title or UUID>”: \`navigatePage\`
78-
- “find when I talked about X / patterns in Y / times I felt Z”: \`semanticJournalSearch\` (optionally bound by \`temporalJournalSearch\`)
79-
- “pull my notes on <topic> across pages; summarize/synthesize”: \`semanticPageSearch\`
80-
- “show me last week/month/quarter entries about <theme>”: \`temporalJournalSearch\` (+ semantic re-ranking if useful)
103+
### \`createPage\`
81104
82-
---
83-
84-
# Global Behavior Meta
105+
Create a new page with the given title, infer the title from the user's prompt and clarify if it's not clear. Use when the user says or implies "create/new/add a page".
85106
86-
- **Important**: If the user is referring to one of the current editors (for example: "today's note", "the page", or similar), FIRST read and search the content of the active editor(s) and answer using those contents. Do not ask the user for anything that you can already access. If no active editor is available, say so and ask which document to use.
87-
- No background or delayed work. Complete tasks in this response.
88-
- Interpret relative time against Current date: ${context.currentDate}.
89-
- Prefer partial completion over clarifying questions when scope is large.
90-
- Mirror the user's tone (e.g., casual or analytical), but avoid corporate filler. Default to casual.
91-
- Quote the user's exact words when it adds clarity or validation. Avoid over-quoting. Be concise and high-signal.
92-
- If the next step is obvious, do it. Example of bad: "bad example: "If you want to see the key insights, I can show them to you.", example of good: "Here are the key insights I found".
93-
- Operate on what exists in Journl and what the user says; never fabricate content. Prefer direct tool actions over prose when the intent is to write, insert, or navigate.
107+
Do not navigate to the page after creating it, it will be done automatically.
94108
95109
---
96110
97-
# User UI State (deterministic, read-only)
111+
# Global Behavior Meta
98112
99-
- Call user by their name: ${context.user.name}.
100-
${
101-
context.view.name === "journal-timeline" && context.view.focusedDate
102-
? `- The user is currently focused on the journal timeline and is engaged with the entry of the date ${context.view.focusedDate}.`
103-
: context.view.name === "journal-entry"
104-
? `- The user is currently focused on the journal entry of the date ${context.view.date}.`
105-
: context.view.name === "page"
106-
? `- The user is currently focused on the page of the UUID ${context.view.id} with the title ${context.view.title}.`
107-
: "- The user is currently on a page without editors."
108-
}
109-
${
110-
context.activeEditors.length > 0
111-
? `- Active editor${context.activeEditors.length > 1 ? "s" : ""}: ${context.activeEditors.join(", ")}.`
112-
: ""
113-
}
114-
${
115-
context.highlightedText.length > 0
116-
? `- User has highlighted: ${context.highlightedText.join(", ")}.`
117-
: ""
118-
}`;
113+
- **Important**: If user refers to current editors ("today's note", "the page"), simply read the content of the active editor(s) for context. Don't ask for information you can already access.
114+
- Complete tasks immediately. Take obvious next steps. Prefer direct tool actions over explanatory prose.
115+
- Mirror user's tone but avoid corporate filler. Be concise and high-signal.
116+
- Operate only on existing content; never fabricate. Prefer partial completion over clarifying questions when scope is large.`;
119117
},
120118
model,
121119
name: AGENT_NAME,
122120
tools: {
121+
createPage,
123122
manipulateEditor,
124123
navigateJournalEntry,
125124
navigatePage,
@@ -130,7 +129,19 @@ ${
130129
});
131130

132131
const zJournlRuntimeContext: z.ZodType<JournlAgentContext> = z.object({
133-
activeEditors: z.array(z.string()),
132+
activeEditors: z.array(
133+
z.union([
134+
z.object({
135+
date: z.string(),
136+
type: z.literal("journal-entry"),
137+
}),
138+
z.object({
139+
id: z.string(),
140+
title: z.string(),
141+
type: z.literal("page"),
142+
}),
143+
]),
144+
),
134145
currentDate: z.string(),
135146
highlightedText: z.array(z.string()),
136147
user: z.object({

apps/web/src/ai/agents/use-journl-agent-awareness.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,21 @@ export type BlockSelection = {
1616
text: string;
1717
};
1818

19+
type JournlEditor = JournlAgentContext["activeEditors"][number] & {
20+
editor: EditorPrimitive;
21+
};
22+
1923
const JournlAgentAwarenessContext = createContext<{
2024
forgetEditor: (id: string) => void;
2125
forgetEditorSelections: (editor: EditorPrimitive) => void;
2226
forgetSelection: (selection: BlockSelection) => void;
23-
getEditors: () => Map<string, EditorPrimitive>;
27+
getEditors: () => Map<string, JournlEditor>;
2428
getSelection: (
2529
selection: Pick<BlockSelection, "editor" | "blockIds">,
2630
) => BlockSelection | undefined;
2731
getSelections: () => BlockSelection[];
2832
getView: () => JournlAgentContext["view"];
29-
rememberEditor: (id: string, editor: EditorPrimitive) => void;
33+
rememberEditor: (activeEditor: JournlEditor) => void;
3034
rememberSelection: (selection: BlockSelection) => void;
3135
rememberView: (view: JournlAgentContext["view"]) => void;
3236
} | null>(null);
@@ -47,7 +51,7 @@ export function JournlAgentAwarenessProvider({
4751
const [_, setSelectedBlocks] = useState<BlockSelection[]>([]);
4852
const ref = useRef<{
4953
view: JournlAgentContext["view"];
50-
editors: Map<string, EditorPrimitive>;
54+
editors: Map<string, JournlEditor>;
5155
selectedBlocks: BlockSelection[];
5256
}>({
5357
editors: new Map(),
@@ -100,8 +104,15 @@ export function JournlAgentAwarenessProvider({
100104
return ref.current.view;
101105
}, []);
102106

103-
const rememberEditor = useCallback((id: string, editor: EditorPrimitive) => {
104-
ref.current.editors.set(id, editor);
107+
const rememberEditor = useCallback((activeEditor: JournlEditor) => {
108+
const id =
109+
activeEditor.type === "journal-entry"
110+
? activeEditor.date
111+
: activeEditor.id;
112+
ref.current.editors.set(id, {
113+
...activeEditor,
114+
editor: activeEditor.editor,
115+
});
105116
}, []);
106117

107118
const rememberSelection = useCallback((selection: BlockSelection) => {

apps/web/src/ai/agents/use-journl-agent.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
type UIMessage,
1010
} from "ai";
1111
import { env } from "~/env";
12+
import { useCreatePageTool } from "../tools/create-page.client";
1213
import { useManipulateEditorTool } from "../tools/manipulate-editor.client";
1314
import { useNavigateJournalEntryTool } from "../tools/navigate-journal-entry.client";
1415
import { useNavigatePageTool } from "../tools/navigate-page.client";
@@ -25,11 +26,13 @@ type UseJournlAgentOptions<Message extends UIMessage = UIMessage> = {} & Pick<
2526

2627
export function useJournlAgent({ transport, messages }: UseJournlAgentOptions) {
2728
const { getEditors, getView, getSelections } = useJournlAgentAwareness();
29+
const createPage = useCreatePageTool();
2830
const navigateJournalEntry = useNavigateJournalEntryTool();
2931
const navigatePage = useNavigatePageTool();
3032
const manipulateEditor = useManipulateEditorTool();
3133

3234
const tools = new Map<string, ClientTool<string, UseChatHelpers<UIMessage>>>([
35+
[createPage.name, createPage],
3336
[navigateJournalEntry.name, navigateJournalEntry],
3437
[navigatePage.name, navigatePage],
3538
[manipulateEditor.name, manipulateEditor],
@@ -55,13 +58,14 @@ export function useJournlAgent({ transport, messages }: UseJournlAgentOptions) {
5558
transport: new DefaultChatTransport({
5659
...transport,
5760
prepareSendMessagesRequest: ({ id, messages, trigger, messageId }) => {
58-
const activeEditors = Array.from(getEditors().keys());
5961
const selections = getSelections();
6062
const view = getView();
6163
return {
6264
body: {
6365
context: {
64-
activeEditors,
66+
activeEditors: Array.from(getEditors().entries()).map(
67+
([_, { editor, ...rest }]) => rest,
68+
),
6569
currentDate: new Date().toLocaleString(),
6670
highlightedText: selections.map((selection) => selection.text),
6771
view,
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"use client";
2+
3+
import type { Page } from "@acme/db/schema";
4+
import { useMutation, useQueryClient } from "@tanstack/react-query";
5+
import { useRouter } from "next/navigation";
6+
import { useTRPC } from "~/trpc/react";
7+
import { useAppEventEmitter } from "../../components/events/app-event-context";
8+
import { PageCreatedEvent } from "../../events/page-created-event";
9+
import { createClientTool } from "../utils/create-client-tool";
10+
import { zCreatePageInput } from "./create-page.schema";
11+
12+
export function useCreatePageTool() {
13+
const router = useRouter();
14+
const trpc = useTRPC();
15+
const queryClient = useQueryClient();
16+
const eventEmitter = useAppEventEmitter();
17+
18+
const { mutate: createPage } = useMutation(
19+
trpc.pages.create.mutationOptions({}),
20+
);
21+
22+
const tool = createClientTool({
23+
execute: async (toolCall, chat) => {
24+
createPage(
25+
{
26+
children: [],
27+
title: toolCall.input.title,
28+
},
29+
{
30+
onError: (error) => {
31+
console.error("Failed to create page:", error);
32+
void chat.addToolResult({
33+
output: `Failed to create page ${toolCall.input.title}`,
34+
tool: toolCall.toolName,
35+
toolCallId: toolCall.toolCallId,
36+
});
37+
},
38+
onSuccess: (newPage) => {
39+
queryClient.setQueryData(
40+
trpc.pages.getByUser.queryOptions().queryKey,
41+
(oldPages: Page[] | undefined) => {
42+
if (!oldPages) return [newPage];
43+
return [newPage, ...oldPages];
44+
},
45+
);
46+
47+
eventEmitter.buffer(
48+
new PageCreatedEvent({
49+
chat,
50+
id: newPage.id,
51+
title: toolCall.input.title,
52+
toolCallId: toolCall.toolCallId,
53+
toolName: toolCall.toolName,
54+
}),
55+
);
56+
57+
router.push(`/pages/${newPage.id}`);
58+
},
59+
},
60+
);
61+
},
62+
inputSchema: zCreatePageInput,
63+
name: "createPage",
64+
});
65+
return tool;
66+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { z } from "zod";
2+
3+
export const zCreatePageInput = z.object({
4+
title: z.string().describe("The title of the page to create."),
5+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { createTool } from "@mastra/core/tools";
2+
import { z } from "zod";
3+
import { zCreatePageInput } from "./create-page.schema";
4+
5+
export const createPage = createTool({
6+
description: "A client-side tool that creates a new page",
7+
id: "create-page",
8+
inputSchema: zCreatePageInput,
9+
outputSchema: z.void(),
10+
});

apps/web/src/ai/tools/manipulate-editor.client.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,20 @@ export function useManipulateEditorTool() {
1212
const tool = createClientTool({
1313
execute: async (toolCall, chat) => {
1414
try {
15-
const editor = getEditors().get(toolCall.input.editorId);
15+
const editor = getEditors().get(toolCall.input.targetEditor)?.editor;
1616

1717
if (!editor) {
18-
const availableEditors = JSON.stringify(
19-
Object.fromEntries(getEditors().entries()),
18+
const activeEditors = JSON.stringify(
19+
Array.from(getEditors().values()).map(
20+
({ editor, ...rest }) => rest,
21+
),
2022
);
21-
return await chat.addToolResult({
22-
output: `Editor ${toolCall.input.editorId} was not found. Please call the tool again with the available editors: ${availableEditors}`,
23+
void chat.addToolResult({
24+
output: `Editor ${toolCall.input.targetEditor} was not found. Please call the tool again targeting one of the following editors: ${activeEditors}`,
2325
tool: toolCall.toolName,
2426
toolCallId: toolCall.toolCallId,
2527
});
28+
return;
2629
}
2730

2831
const aiExtension = getAIExtension(editor);

apps/web/src/ai/tools/manipulate-editor.schema.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { z } from "zod";
22

33
export const zManipulateEditorInput = z.object({
4-
editorId: z
4+
targetEditor: z
55
.string()
66
.describe(
7-
"The ID of one of the active editors to manipulate. It is the date of a journal entry (in YYYY-MM-DD format) or a page ID (UUID).",
7+
"The target editor to manipulate. It is the date of a journal entry (in YYYY-MM-DD format) or a page ID (UUID format).",
88
),
99
userPrompt: z
1010
.string()

0 commit comments

Comments
 (0)