feat: add get_my_profile tool and max iterations limit

- Create builtin-tools.ts with get_my_profile tool that returns the
  logged-in user's Nostr profile metadata
- Add MAX_TOOL_ITERATIONS (10) to prevent infinite agentic loops
- Auto-register builtin tools on session manager import

https://claude.ai/code/session_01HqtD9R33oqfB14Gu1V5wHC
This commit is contained in:
Claude
2026-01-31 12:29:35 +00:00
parent fa70933388
commit afb0ea1e81
2 changed files with 125 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
/**
* Built-in Tools for AI Chat
*
* These tools are automatically registered with the tool registry.
*/
import { firstValueFrom, filter, timeout, catchError, of } from "rxjs";
import { getProfileContent } from "applesauce-core/helpers";
import accounts from "@/services/accounts";
import eventStore from "@/services/event-store";
import { toolRegistry, type Tool } from "./tools";
// ─────────────────────────────────────────────────────────────
// get_my_profile - Returns the logged-in user's profile
// ─────────────────────────────────────────────────────────────
const getMyProfileTool: Tool = {
name: "get_my_profile",
description:
"Get the profile information of the currently logged-in user. Returns their display name, about, picture, and other metadata. Returns an error if no user is logged in.",
parameters: {
type: "object",
properties: {},
required: [],
},
async execute(_args, _context) {
// Get the active account
const account = accounts.active$.getValue();
if (!account) {
return {
success: false,
content: "",
error: "No user is currently logged in",
};
}
const pubkey = account.pubkey;
try {
// Try to get the profile from the event store
const profileEvent = await firstValueFrom(
eventStore.replaceable(0, pubkey).pipe(
filter((event) => event !== undefined),
timeout(5000),
catchError(() => of(undefined)),
),
);
if (!profileEvent) {
return {
success: true,
content: JSON.stringify({
pubkey,
profile: null,
message: "User is logged in but no profile metadata found",
}),
};
}
const profile = getProfileContent(profileEvent);
if (!profile) {
return {
success: true,
content: JSON.stringify({
pubkey,
profile: null,
message:
"User is logged in but profile metadata could not be parsed",
}),
};
}
return {
success: true,
content: JSON.stringify({
pubkey,
profile: {
name: profile.name,
display_name: profile.display_name,
about: profile.about,
picture: profile.picture,
banner: profile.banner,
website: profile.website,
nip05: profile.nip05,
lud16: profile.lud16,
},
}),
};
} catch (error) {
return {
success: false,
content: "",
error: `Failed to fetch profile: ${error instanceof Error ? error.message : "Unknown error"}`,
};
}
},
};
// ─────────────────────────────────────────────────────────────
// Register all built-in tools
// ─────────────────────────────────────────────────────────────
export function registerBuiltinTools(): void {
toolRegistry.register(getMyProfileTool);
}
// Auto-register on import
registerBuiltinTools();

View File

@@ -14,6 +14,7 @@ import { BehaviorSubject, Subject } from "rxjs";
import db from "@/services/db";
import { providerManager } from "./provider-manager";
import { toolRegistry, executeToolCalls, type ToolContext } from "./tools";
import "@/services/llm/builtin-tools"; // Register built-in tools
import type {
ChatSessionState,
StreamingUpdateEvent,
@@ -31,6 +32,9 @@ import type {
// Session cleanup delay (ms) - wait before cleaning up after last subscriber leaves
const CLEANUP_DELAY = 5000;
// Maximum tool execution iterations to prevent infinite loops
const MAX_TOOL_ITERATIONS = 10;
class ChatSessionManager {
// ─────────────────────────────────────────────────────────────
// Reactive State
@@ -340,8 +344,19 @@ class ChatSessionManager {
// Agentic loop - continue until we get a final response
let continueLoop = true;
let totalCost = 0;
let iterations = 0;
while (continueLoop) {
iterations++;
// Safety check: prevent infinite loops
if (iterations > MAX_TOOL_ITERATIONS) {
console.warn(
`[SessionManager] Max tool iterations (${MAX_TOOL_ITERATIONS}) reached for conversation ${conversationId}`,
);
break;
}
// Check if aborted
if (abortController.signal.aborted) {
throw new DOMException("Aborted", "AbortError");