2026-06-14 16:05:04 +01:00
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 { createQueryDebugTools } from "./debug-tools.ts"
interface CreateFreyaAgentToolsConfig {
userId : string
sessionManager : UserSessionManager
}
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"
2026-06-14 23:08:28 +01:00
export const FREYA_EXECUTE_ACTION_TOOL = "freya_execute_action"
2026-06-14 16:05:04 +01:00
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 ,
2026-06-14 23:08:28 +01:00
FREYA_EXECUTE_ACTION_TOOL ,
2026-06-14 16:05:04 +01:00
]
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 ) ,
} )
2026-06-14 23:08:28 +01:00
const executeActionTool = defineTool ( {
name : FREYA_EXECUTE_ACTION_TOOL ,
label : "Execute FREYA Action" ,
description :
"Execute an available FREYA source action immediately without creating a proposal." ,
2026-06-14 16:05:04 +01:00
parameters : Type.Object ( {
2026-06-14 23:08:28 +01:00
sourceId : Type.String ( { description : "Source ID that should execute the action." } ) ,
actionId : Type.String ( { description : "Source action ID to execute." } ) ,
2026-06-14 16:05:04 +01:00
params : Type.Optional (
Type . Unknown ( {
2026-06-14 23:08:28 +01:00
description : "Parameters to pass to the source action." ,
2026-06-14 16:05:04 +01:00
} ) ,
) ,
} ) ,
2026-06-14 23:08:28 +01:00
execute : async ( _toolCallId , params ) = > executeActionToolCall ( config , params ) ,
2026-06-14 16:05:04 +01:00
} )
return [
listSourcesTool ,
getContextTool ,
getFeedItemTool ,
queryContextTool ,
listContextTool ,
getSourceDataTool ,
2026-06-14 23:08:28 +01:00
executeActionTool ,
2026-06-14 16:05:04 +01:00
]
}
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 : { } ,
}
}
2026-06-14 23:08:28 +01:00
async function executeActionToolCall (
2026-06-14 16:05:04 +01:00
config : CreateFreyaAgentToolsConfig ,
params : {
2026-06-14 23:08:28 +01:00
sourceId : string
actionId : string
2026-06-14 16:05:04 +01:00
params? : unknown
} ,
) {
2026-06-14 23:08:28 +01:00
const userSession = await config . sessionManager . getOrCreate ( config . userId )
const result = await userSession . engine . executeAction (
params . sourceId ,
params . actionId ,
params . params ,
)
2026-06-14 16:05:04 +01:00
2026-06-14 23:08:28 +01:00
const actionExecution = {
sourceId : params.sourceId ,
actionId : params.actionId ,
result : result ? ? null ,
}
2026-06-14 16:05:04 +01:00
return {
content : [
{
type : "text" as const ,
text : JSON.stringify ( {
ok : true ,
2026-06-14 23:08:28 +01:00
. . . actionExecution ,
2026-06-14 16:05:04 +01:00
} ) ,
} ,
] ,
2026-06-14 23:08:28 +01:00
details : { actionExecution } ,
2026-06-14 16:05:04 +01:00
}
}