mirror of
https://github.com/kennethnym/freya
synced 2026-07-04 15:11:15 +01:00
325 lines
9.5 KiB
TypeScript
325 lines
9.5 KiB
TypeScript
|
|
import { defineTool } from "@earendil-works/pi-coding-agent"
|
||
|
|
import { Type } from "typebox"
|
||
|
|
|
||
|
|
import type { UserSessionManager } from "../session/index.ts"
|
||
|
|
import type { QueryDebugTools } from "./debug-tools.ts"
|
||
|
|
import type { ProposedAction } from "./query-agent.ts"
|
||
|
|
|
||
|
|
import { createQueryDebugTools } from "./debug-tools.ts"
|
||
|
|
|
||
|
|
interface CreateFreyaAgentToolsConfig {
|
||
|
|
userId: string
|
||
|
|
sessionManager: UserSessionManager
|
||
|
|
clock: () => Date
|
||
|
|
proposeAction(action: ProposedAction): void
|
||
|
|
}
|
||
|
|
|
||
|
|
export const FREYA_QUERY_CONTEXT_TOOL = "freya_query_context"
|
||
|
|
export const FREYA_LIST_SOURCES_TOOL = "freya_list_sources"
|
||
|
|
export const FREYA_GET_CONTEXT_TOOL = "freya_get_context"
|
||
|
|
export const FREYA_LIST_CONTEXT_TOOL = "freya_list_context"
|
||
|
|
export const FREYA_GET_SOURCE_DATA_TOOL = "freya_get_source_data"
|
||
|
|
export const FREYA_GET_FEED_ITEM_TOOL = "freya_get_feed_item"
|
||
|
|
export const FREYA_PROPOSE_ACTION_TOOL = "freya_propose_action"
|
||
|
|
|
||
|
|
export const FREYA_AGENT_TOOL_NAMES = [
|
||
|
|
FREYA_LIST_SOURCES_TOOL,
|
||
|
|
FREYA_GET_CONTEXT_TOOL,
|
||
|
|
FREYA_GET_FEED_ITEM_TOOL,
|
||
|
|
FREYA_QUERY_CONTEXT_TOOL,
|
||
|
|
FREYA_LIST_CONTEXT_TOOL,
|
||
|
|
FREYA_GET_SOURCE_DATA_TOOL,
|
||
|
|
FREYA_PROPOSE_ACTION_TOOL,
|
||
|
|
]
|
||
|
|
|
||
|
|
export function createFreyaAgentTools(config: CreateFreyaAgentToolsConfig) {
|
||
|
|
const { userId } = config
|
||
|
|
const debugTools = createQueryDebugTools(config.sessionManager)
|
||
|
|
|
||
|
|
const listSourcesTool = defineTool({
|
||
|
|
name: FREYA_LIST_SOURCES_TOOL,
|
||
|
|
label: "List FREYA Sources",
|
||
|
|
description:
|
||
|
|
"List enabled FREYA source IDs and summarize available feed items, context entries, actions, and errors.",
|
||
|
|
parameters: Type.Object({}),
|
||
|
|
execute: async () => executeDebugTool(debugTools, userId, FREYA_LIST_SOURCES_TOOL, {}),
|
||
|
|
})
|
||
|
|
|
||
|
|
const getContextTool = defineTool({
|
||
|
|
name: FREYA_GET_CONTEXT_TOOL,
|
||
|
|
label: "Get FREYA Context",
|
||
|
|
description:
|
||
|
|
"Read specific FREYA context entries by key. Use prefix matching to discover entries under a source ID, or exact matching when you know the full key.",
|
||
|
|
parameters: Type.Object({
|
||
|
|
key: Type.Array(Type.Unknown(), {
|
||
|
|
description:
|
||
|
|
'Context key array, for example ["freya.location"] or ["freya.location", "location"].',
|
||
|
|
}),
|
||
|
|
match: Type.Optional(
|
||
|
|
Type.Union([Type.Literal("exact"), Type.Literal("prefix")], {
|
||
|
|
description: "Match mode. Defaults to prefix.",
|
||
|
|
}),
|
||
|
|
),
|
||
|
|
}),
|
||
|
|
execute: async (_toolCallId, params) =>
|
||
|
|
executeDebugTool(debugTools, userId, FREYA_GET_CONTEXT_TOOL, params),
|
||
|
|
})
|
||
|
|
|
||
|
|
const getFeedItemTool = defineTool({
|
||
|
|
name: FREYA_GET_FEED_ITEM_TOOL,
|
||
|
|
label: "Get FREYA Feed Item",
|
||
|
|
description: "Read one feed item by ID, including related source context, actions, and errors.",
|
||
|
|
parameters: Type.Object({
|
||
|
|
feedItemId: Type.String({ description: "Feed item ID to inspect." }),
|
||
|
|
}),
|
||
|
|
execute: async (_toolCallId, params) =>
|
||
|
|
executeDebugTool(debugTools, userId, FREYA_GET_FEED_ITEM_TOOL, params),
|
||
|
|
})
|
||
|
|
|
||
|
|
const queryContextTool = defineTool({
|
||
|
|
name: FREYA_QUERY_CONTEXT_TOOL,
|
||
|
|
label: "Query FREYA Context",
|
||
|
|
description:
|
||
|
|
"Read the user's current FREYA feed, source graph context, source errors, and available actions.",
|
||
|
|
parameters: Type.Object({
|
||
|
|
question: Type.String({
|
||
|
|
description: "The specific personal-context question to answer.",
|
||
|
|
}),
|
||
|
|
feedItemId: Type.Optional(
|
||
|
|
Type.String({
|
||
|
|
description: "Optional feed item ID when the user is asking about a specific card.",
|
||
|
|
}),
|
||
|
|
),
|
||
|
|
}),
|
||
|
|
execute: async (_toolCallId, params) => executeQueryContextTool(config, params),
|
||
|
|
})
|
||
|
|
|
||
|
|
const listContextTool = defineTool({
|
||
|
|
name: FREYA_LIST_CONTEXT_TOOL,
|
||
|
|
label: "List FREYA Context",
|
||
|
|
description:
|
||
|
|
"List all current FREYA context graph entries for the user. Use this to inspect what personal context is available.",
|
||
|
|
parameters: Type.Object({}),
|
||
|
|
execute: async () => executeListContextTool(config),
|
||
|
|
})
|
||
|
|
|
||
|
|
const getSourceDataTool = defineTool({
|
||
|
|
name: FREYA_GET_SOURCE_DATA_TOOL,
|
||
|
|
label: "Get FREYA Source Data",
|
||
|
|
description:
|
||
|
|
"Get current feed items, context entries, actions, and errors for a specific FREYA source ID.",
|
||
|
|
parameters: Type.Object({
|
||
|
|
sourceId: Type.String({
|
||
|
|
description: "Source ID, for example freya.location, freya.tfl, or freya.weather.",
|
||
|
|
}),
|
||
|
|
feedItemId: Type.Optional(
|
||
|
|
Type.String({
|
||
|
|
description: "Optional feed item ID to select one item from the source.",
|
||
|
|
}),
|
||
|
|
),
|
||
|
|
}),
|
||
|
|
execute: async (_toolCallId, params) => executeGetSourceDataTool(config, params),
|
||
|
|
})
|
||
|
|
|
||
|
|
const proposeActionTool = defineTool({
|
||
|
|
name: FREYA_PROPOSE_ACTION_TOOL,
|
||
|
|
label: "Propose FREYA Action",
|
||
|
|
description: "Create a proposed action for the user to review. This never executes the action.",
|
||
|
|
parameters: Type.Object({
|
||
|
|
title: Type.String({ description: "Short user-facing action title." }),
|
||
|
|
description: Type.String({
|
||
|
|
description: "What will happen if the user confirms this action.",
|
||
|
|
}),
|
||
|
|
sourceId: Type.Optional(
|
||
|
|
Type.String({ description: "Source ID that should execute the action, if known." }),
|
||
|
|
),
|
||
|
|
actionId: Type.Optional(
|
||
|
|
Type.String({ description: "Source action ID to execute after confirmation, if known." }),
|
||
|
|
),
|
||
|
|
params: Type.Optional(
|
||
|
|
Type.Unknown({
|
||
|
|
description: "Parameters to pass to the source action after confirmation.",
|
||
|
|
}),
|
||
|
|
),
|
||
|
|
}),
|
||
|
|
execute: async (_toolCallId, params) => executeProposeActionTool(config, params),
|
||
|
|
})
|
||
|
|
|
||
|
|
return [
|
||
|
|
listSourcesTool,
|
||
|
|
getContextTool,
|
||
|
|
getFeedItemTool,
|
||
|
|
queryContextTool,
|
||
|
|
listContextTool,
|
||
|
|
getSourceDataTool,
|
||
|
|
proposeActionTool,
|
||
|
|
]
|
||
|
|
}
|
||
|
|
|
||
|
|
async function executeDebugTool(
|
||
|
|
debugTools: QueryDebugTools,
|
||
|
|
userId: string,
|
||
|
|
toolName: string,
|
||
|
|
params: unknown,
|
||
|
|
) {
|
||
|
|
const result = await debugTools.execute(userId, toolName, params)
|
||
|
|
|
||
|
|
return {
|
||
|
|
content: [
|
||
|
|
{
|
||
|
|
type: "text" as const,
|
||
|
|
text: JSON.stringify(result),
|
||
|
|
},
|
||
|
|
],
|
||
|
|
details: {},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function executeQueryContextTool(
|
||
|
|
config: CreateFreyaAgentToolsConfig,
|
||
|
|
params: { question: string; feedItemId?: string },
|
||
|
|
) {
|
||
|
|
const userSession = await config.sessionManager.getOrCreate(config.userId)
|
||
|
|
const feed = await userSession.feed()
|
||
|
|
const context = userSession.engine.currentContext()
|
||
|
|
const feedItemId = params.feedItemId
|
||
|
|
const selectedItem =
|
||
|
|
typeof feedItemId === "string" ? feed.items.find((item) => item.id === feedItemId) : undefined
|
||
|
|
const actions = await userSession.listActions()
|
||
|
|
|
||
|
|
return {
|
||
|
|
content: [
|
||
|
|
{
|
||
|
|
type: "text" as const,
|
||
|
|
text: JSON.stringify({
|
||
|
|
time: context.time.toISOString(),
|
||
|
|
question: params.question,
|
||
|
|
feedItemId: feedItemId ?? null,
|
||
|
|
selectedItem: selectedItem ?? null,
|
||
|
|
items: feed.items,
|
||
|
|
context: context.entries(),
|
||
|
|
availableActions: actions.map((entry) => ({
|
||
|
|
sourceId: entry.sourceId,
|
||
|
|
actions: Object.values(entry.actions).map((action) => ({
|
||
|
|
id: action.id,
|
||
|
|
description: action.description ?? null,
|
||
|
|
})),
|
||
|
|
})),
|
||
|
|
errors: feed.errors.map((error) => ({
|
||
|
|
sourceId: error.sourceId,
|
||
|
|
message: error.error.message,
|
||
|
|
})),
|
||
|
|
}),
|
||
|
|
},
|
||
|
|
],
|
||
|
|
details: {},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function executeListContextTool(config: CreateFreyaAgentToolsConfig) {
|
||
|
|
const userSession = await config.sessionManager.getOrCreate(config.userId)
|
||
|
|
await userSession.feed()
|
||
|
|
const context = userSession.engine.currentContext()
|
||
|
|
const entries = context.entries()
|
||
|
|
|
||
|
|
return {
|
||
|
|
content: [
|
||
|
|
{
|
||
|
|
type: "text" as const,
|
||
|
|
text: JSON.stringify({
|
||
|
|
time: context.time.toISOString(),
|
||
|
|
count: entries.length,
|
||
|
|
entries,
|
||
|
|
}),
|
||
|
|
},
|
||
|
|
],
|
||
|
|
details: {},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function executeGetSourceDataTool(
|
||
|
|
config: CreateFreyaAgentToolsConfig,
|
||
|
|
params: { sourceId: string; feedItemId?: string },
|
||
|
|
) {
|
||
|
|
const userSession = await config.sessionManager.getOrCreate(config.userId)
|
||
|
|
const feed = await userSession.feed()
|
||
|
|
const context = userSession.engine.currentContext()
|
||
|
|
const sourceActions = userSession.hasSource(params.sourceId)
|
||
|
|
? await userSession.engine.listActions(params.sourceId)
|
||
|
|
: {}
|
||
|
|
|
||
|
|
const items = feed.items.filter((item) => item.sourceId === params.sourceId)
|
||
|
|
const selectedItem =
|
||
|
|
params.feedItemId !== undefined
|
||
|
|
? items.find((item) => item.id === params.feedItemId)
|
||
|
|
: undefined
|
||
|
|
const contextEntries = context.entries().filter((entry) => entry.key[0] === params.sourceId)
|
||
|
|
const errors = feed.errors
|
||
|
|
.filter((error) => error.sourceId === params.sourceId)
|
||
|
|
.map((error) => ({
|
||
|
|
sourceId: error.sourceId,
|
||
|
|
message: error.error.message,
|
||
|
|
}))
|
||
|
|
|
||
|
|
return {
|
||
|
|
content: [
|
||
|
|
{
|
||
|
|
type: "text" as const,
|
||
|
|
text: JSON.stringify({
|
||
|
|
time: context.time.toISOString(),
|
||
|
|
sourceId: params.sourceId,
|
||
|
|
hasSource: userSession.hasSource(params.sourceId),
|
||
|
|
feedItemId: params.feedItemId ?? null,
|
||
|
|
selectedItem: selectedItem ?? null,
|
||
|
|
items,
|
||
|
|
context: contextEntries,
|
||
|
|
actions: Object.values(sourceActions).map((action) => ({
|
||
|
|
id: action.id,
|
||
|
|
description: action.description ?? null,
|
||
|
|
})),
|
||
|
|
errors,
|
||
|
|
}),
|
||
|
|
},
|
||
|
|
],
|
||
|
|
details: {},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function executeProposeActionTool(
|
||
|
|
config: CreateFreyaAgentToolsConfig,
|
||
|
|
params: {
|
||
|
|
title: string
|
||
|
|
description: string
|
||
|
|
sourceId?: string
|
||
|
|
actionId?: string
|
||
|
|
params?: unknown
|
||
|
|
},
|
||
|
|
) {
|
||
|
|
const action: ProposedAction = {
|
||
|
|
id: crypto.randomUUID(),
|
||
|
|
title: params.title,
|
||
|
|
description: params.description,
|
||
|
|
requiresConfirmation: true,
|
||
|
|
createdAt: config.clock().toISOString(),
|
||
|
|
...(params.sourceId ? { sourceId: params.sourceId } : {}),
|
||
|
|
...(params.actionId ? { actionId: params.actionId } : {}),
|
||
|
|
...(params.params !== undefined ? { params: params.params } : {}),
|
||
|
|
}
|
||
|
|
|
||
|
|
config.proposeAction(action)
|
||
|
|
|
||
|
|
return {
|
||
|
|
content: [
|
||
|
|
{
|
||
|
|
type: "text" as const,
|
||
|
|
text: JSON.stringify({
|
||
|
|
ok: true,
|
||
|
|
proposedActionId: action.id,
|
||
|
|
requiresConfirmation: true,
|
||
|
|
}),
|
||
|
|
},
|
||
|
|
],
|
||
|
|
details: { proposedAction: action },
|
||
|
|
}
|
||
|
|
}
|