mirror of
https://github.com/kennethnym/aris.git
synced 2026-06-15 20:11:18 +01:00
Compare commits
1 Commits
master
...
build/lock
| Author | SHA1 | Date | |
|---|---|---|---|
|
61d2245261
|
@@ -15,8 +15,20 @@ interface AuthSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ProposedAction {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
sourceId?: string
|
||||||
|
actionId?: string
|
||||||
|
params?: unknown
|
||||||
|
requiresConfirmation: true
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
interface QueryResponse {
|
interface QueryResponse {
|
||||||
message: string
|
message: string
|
||||||
|
proposedActions: ProposedAction[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QueryToolDefinition {
|
interface QueryToolDefinition {
|
||||||
@@ -175,6 +187,7 @@ async function askAgent(backendUrl: string, cookies: CookieJar, message: string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`\nagent> ${data.message || "(no message)"}`)
|
console.log(`\nagent> ${data.message || "(no message)"}`)
|
||||||
|
printProposedActions(data.proposedActions)
|
||||||
console.log("")
|
console.log("")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,6 +366,22 @@ function printHelp(): void {
|
|||||||
console.log(" /quit Exit\n")
|
console.log(" /quit Exit\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function printProposedActions(actions: ProposedAction[]): void {
|
||||||
|
if (actions.length === 0) return
|
||||||
|
|
||||||
|
console.log("\nProposed actions:")
|
||||||
|
for (const action of actions) {
|
||||||
|
console.log(`- ${action.title} (${action.id})`)
|
||||||
|
console.log(` ${action.description}`)
|
||||||
|
if (action.sourceId || action.actionId) {
|
||||||
|
console.log(` source=${action.sourceId ?? "-"} action=${action.actionId ?? "-"}`)
|
||||||
|
}
|
||||||
|
if (action.params !== undefined) {
|
||||||
|
console.log(` params=${JSON.stringify(action.params)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function askRequired(
|
function askRequired(
|
||||||
label: string,
|
label: string,
|
||||||
defaultValue?: string,
|
defaultValue?: string,
|
||||||
@@ -550,7 +579,9 @@ function isAuthSession(value: unknown): value is AuthSession {
|
|||||||
|
|
||||||
function isQueryResponse(value: unknown): value is QueryResponse {
|
function isQueryResponse(value: unknown): value is QueryResponse {
|
||||||
if (!isJsonObject(value)) return false
|
if (!isJsonObject(value)) return false
|
||||||
return typeof value.message === "string"
|
if (typeof value.message !== "string") return false
|
||||||
|
if (!Array.isArray(value.proposedActions)) return false
|
||||||
|
return value.proposedActions.every(isProposedAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isQueryToolsResponse(value: unknown): value is QueryToolsResponse {
|
function isQueryToolsResponse(value: unknown): value is QueryToolsResponse {
|
||||||
@@ -585,6 +616,20 @@ function isSourceActionDefinition(value: unknown): value is { id: string; descri
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isProposedAction(value: unknown): value is ProposedAction {
|
||||||
|
if (!isJsonObject(value)) return false
|
||||||
|
|
||||||
|
return (
|
||||||
|
typeof value.id === "string" &&
|
||||||
|
typeof value.title === "string" &&
|
||||||
|
typeof value.description === "string" &&
|
||||||
|
(value.sourceId === undefined || typeof value.sourceId === "string") &&
|
||||||
|
(value.actionId === undefined || typeof value.actionId === "string") &&
|
||||||
|
value.requiresConfirmation === true &&
|
||||||
|
typeof value.createdAt === "string"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function isJsonObject(value: unknown): value is JsonObject {
|
function isJsonObject(value: unknown): value is JsonObject {
|
||||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,28 +57,6 @@ describe("query debug tools", () => {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("executes source action directly", async () => {
|
|
||||||
const tools = createTestDebugTools()
|
|
||||||
const params = { title: "Buy tea" }
|
|
||||||
|
|
||||||
const result = await tools.execute("user-1", "freya_execute_action", {
|
|
||||||
sourceId: "freya.reminders",
|
|
||||||
actionId: "create-reminder",
|
|
||||||
params,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(result).toEqual({
|
|
||||||
ok: true,
|
|
||||||
sourceId: "freya.reminders",
|
|
||||||
actionId: "create-reminder",
|
|
||||||
result: {
|
|
||||||
sourceId: "freya.reminders",
|
|
||||||
actionId: "create-reminder",
|
|
||||||
params,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function createTestDebugTools() {
|
function createTestDebugTools() {
|
||||||
@@ -131,16 +109,6 @@ function createTestDebugTools() {
|
|||||||
async listActions(sourceId: string) {
|
async listActions(sourceId: string) {
|
||||||
return actions[sourceId] ?? {}
|
return actions[sourceId] ?? {}
|
||||||
},
|
},
|
||||||
async executeAction(sourceId: string, actionId: string, params: unknown) {
|
|
||||||
const sourceActions = actions[sourceId]
|
|
||||||
if (!sourceActions) {
|
|
||||||
throw new Error(`Source not found: ${sourceId}`)
|
|
||||||
}
|
|
||||||
if (!(actionId in sourceActions)) {
|
|
||||||
throw new Error(`Action "${actionId}" not found on source "${sourceId}"`)
|
|
||||||
}
|
|
||||||
return { sourceId, actionId, params }
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
hasSource(sourceId: string) {
|
hasSource(sourceId: string) {
|
||||||
return sourceId in actions
|
return sourceId in actions
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { contextKey, type ContextKeyPart } from "@freya/core"
|
import { contextKey, type ContextKeyPart } from "@freya/core"
|
||||||
|
|
||||||
import type { UserSessionManager } from "../session/index.ts"
|
import type { UserSessionManager } from "../session/index.ts"
|
||||||
|
import type { ProposedAction } from "./query-agent.ts"
|
||||||
|
|
||||||
type ToolParams = Record<string, unknown>
|
type ToolParams = Record<string, unknown>
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ const FreyaGetContextTool = "freya_get_context"
|
|||||||
const FreyaListContextTool = "freya_list_context"
|
const FreyaListContextTool = "freya_list_context"
|
||||||
const FreyaGetSourceDataTool = "freya_get_source_data"
|
const FreyaGetSourceDataTool = "freya_get_source_data"
|
||||||
const FreyaGetFeedItemTool = "freya_get_feed_item"
|
const FreyaGetFeedItemTool = "freya_get_feed_item"
|
||||||
const FreyaExecuteActionTool = "freya_execute_action"
|
const FreyaProposeActionTool = "freya_propose_action"
|
||||||
|
|
||||||
export function createQueryDebugTools(sessionManager: UserSessionManager): QueryDebugTools {
|
export function createQueryDebugTools(sessionManager: UserSessionManager): QueryDebugTools {
|
||||||
return new DefaultQueryDebugTools(sessionManager)
|
return new DefaultQueryDebugTools(sessionManager)
|
||||||
@@ -85,12 +86,14 @@ class DefaultQueryDebugTools implements QueryDebugTools {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: FreyaExecuteActionTool,
|
name: FreyaProposeActionTool,
|
||||||
label: "Execute FREYA Action",
|
label: "Propose FREYA Action",
|
||||||
description: "Execute an available source action immediately.",
|
description: "Create a proposed action object without executing it.",
|
||||||
parameters: {
|
parameters: {
|
||||||
sourceId: "string",
|
title: "string",
|
||||||
actionId: "string",
|
description: "string",
|
||||||
|
sourceId: "string?",
|
||||||
|
actionId: "string?",
|
||||||
params: "unknown?",
|
params: "unknown?",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -111,8 +114,8 @@ class DefaultQueryDebugTools implements QueryDebugTools {
|
|||||||
return this.listContext(userId)
|
return this.listContext(userId)
|
||||||
case FreyaGetSourceDataTool:
|
case FreyaGetSourceDataTool:
|
||||||
return this.getSourceData(userId, expectToolParams(params, ["sourceId"]))
|
return this.getSourceData(userId, expectToolParams(params, ["sourceId"]))
|
||||||
case FreyaExecuteActionTool:
|
case FreyaProposeActionTool:
|
||||||
return this.executeAction(userId, expectToolParams(params, ["sourceId", "actionId"]))
|
return proposeAction(expectToolParams(params, ["title", "description"]))
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown debug tool: ${toolName}`)
|
throw new Error(`Unknown debug tool: ${toolName}`)
|
||||||
}
|
}
|
||||||
@@ -319,20 +322,27 @@ class DefaultQueryDebugTools implements QueryDebugTools {
|
|||||||
errors,
|
errors,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async executeAction(userId: string, params: ToolParams): Promise<unknown> {
|
function proposeAction(params: ToolParams): unknown {
|
||||||
const sourceId = expectString(params, "sourceId")
|
const sourceId = optionalString(params, "sourceId")
|
||||||
const actionId = expectString(params, "actionId")
|
const actionId = optionalString(params, "actionId")
|
||||||
const actionParams = "params" in params ? params.params : undefined
|
const action: ProposedAction = {
|
||||||
const userSession = await this.sessionManager.getOrCreate(userId)
|
id: crypto.randomUUID(),
|
||||||
const result = await userSession.engine.executeAction(sourceId, actionId, actionParams)
|
title: expectString(params, "title"),
|
||||||
|
description: expectString(params, "description"),
|
||||||
|
requiresConfirmation: true,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
...(sourceId ? { sourceId } : {}),
|
||||||
|
...(actionId ? { actionId } : {}),
|
||||||
|
...("params" in params ? { params: params.params } : {}),
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
sourceId,
|
proposedActionId: action.id,
|
||||||
actionId,
|
requiresConfirmation: true,
|
||||||
result: result ?? null,
|
proposedAction: action,
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"
|
|||||||
import { Hono } from "hono"
|
import { Hono } from "hono"
|
||||||
|
|
||||||
import type { QueryDebugTools, QueryDebugToolDefinition } from "./debug-tools.ts"
|
import type { QueryDebugTools, QueryDebugToolDefinition } from "./debug-tools.ts"
|
||||||
import type { QueryAgent, QueryAgentAsk, QueryAgentEvent } from "./query-agent.ts"
|
import type { ProposedAction, QueryAgent, QueryAgentAsk, QueryAgentEvent } from "./query-agent.ts"
|
||||||
|
|
||||||
import { mockAuthSessionMiddleware } from "../auth/session-middleware.ts"
|
import { mockAuthSessionMiddleware } from "../auth/session-middleware.ts"
|
||||||
import { registerAgentHttpHandlers, registerDebugAgentHttpHandlers } from "./http.ts"
|
import { registerAgentHttpHandlers, registerDebugAgentHttpHandlers } from "./http.ts"
|
||||||
@@ -80,10 +80,21 @@ describe("POST /api/agent", () => {
|
|||||||
expect(res.status).toBe(401)
|
expect(res.status).toBe(401)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("collects text deltas", async () => {
|
test("collects text deltas and proposed actions", async () => {
|
||||||
|
const action: ProposedAction = {
|
||||||
|
id: "proposal-1",
|
||||||
|
title: "Update commute line",
|
||||||
|
description: "Set the user's commute line to Victoria.",
|
||||||
|
sourceId: "freya.tfl",
|
||||||
|
actionId: "set-lines-of-interest",
|
||||||
|
params: ["victoria"],
|
||||||
|
requiresConfirmation: true,
|
||||||
|
createdAt: "2026-06-12T12:00:00.000Z",
|
||||||
|
}
|
||||||
const agent = new FakeQueryAgent([
|
const agent = new FakeQueryAgent([
|
||||||
{ type: "text_delta", text: "You should " },
|
{ type: "text_delta", text: "You should " },
|
||||||
{ type: "text_delta", text: "leave at 8:30." },
|
{ type: "text_delta", text: "leave at 8:30." },
|
||||||
|
{ type: "action_proposed", action },
|
||||||
{ type: "done" },
|
{ type: "done" },
|
||||||
])
|
])
|
||||||
const app = buildTestApp(agent, "user-1")
|
const app = buildTestApp(agent, "user-1")
|
||||||
@@ -101,8 +112,10 @@ describe("POST /api/agent", () => {
|
|||||||
|
|
||||||
const body = (await res.json()) as {
|
const body = (await res.json()) as {
|
||||||
message: string
|
message: string
|
||||||
|
proposedActions: ProposedAction[]
|
||||||
}
|
}
|
||||||
expect(body.message).toBe("You should leave at 8:30.")
|
expect(body.message).toBe("You should leave at 8:30.")
|
||||||
|
expect(body.proposedActions).toEqual([action])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("returns 400 for invalid body", async () => {
|
test("returns 400 for invalid body", async () => {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
import { tmpdir } from "node:os"
|
import { tmpdir } from "node:os"
|
||||||
|
|
||||||
import type { UserSessionManager } from "../session/index.ts"
|
import type { UserSessionManager } from "../session/index.ts"
|
||||||
import type { QueryAgent, QueryAgentAsk, QueryAgentEvent } from "./query-agent.ts"
|
import type { ProposedAction, QueryAgent, QueryAgentAsk, QueryAgentEvent } from "./query-agent.ts"
|
||||||
|
|
||||||
import { InMemoryResourceLoader } from "./in-memory-resource-loader.ts"
|
import { InMemoryResourceLoader } from "./in-memory-resource-loader.ts"
|
||||||
import defaultSystemPrompt from "./prompts/system.txt"
|
import defaultSystemPrompt from "./prompts/system.txt"
|
||||||
@@ -28,18 +28,24 @@ export interface PiQueryAgentConfig {
|
|||||||
apiKey?: string
|
apiKey?: string
|
||||||
cwd?: string
|
cwd?: string
|
||||||
systemPrompt?: string
|
systemPrompt?: string
|
||||||
|
clock?: () => Date
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActiveRun {
|
||||||
|
proposedActions: ProposedAction[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PiQueryAgent implements QueryAgent {
|
export class PiQueryAgent implements QueryAgent {
|
||||||
private readonly sessionManager: UserSessionManager
|
private readonly sessionManager: UserSessionManager
|
||||||
private readonly cwd: string
|
private readonly cwd: string
|
||||||
private readonly systemPrompt: string
|
private readonly systemPrompt: string
|
||||||
|
private readonly clock: () => Date
|
||||||
private readonly modelProvider: string
|
private readonly modelProvider: string
|
||||||
private readonly modelId: string
|
private readonly modelId: string
|
||||||
private readonly apiKey: string | undefined
|
private readonly apiKey: string | undefined
|
||||||
private readonly sessions = new Map<string, PiSession>()
|
private readonly sessions = new Map<string, PiSession>()
|
||||||
private readonly pendingSessions = new Map<string, Promise<PiSession>>()
|
private readonly pendingSessions = new Map<string, Promise<PiSession>>()
|
||||||
private readonly activeRuns = new Map<string, symbol>()
|
private readonly activeRuns = new Map<string, ActiveRun>()
|
||||||
|
|
||||||
constructor(config: PiQueryAgentConfig) {
|
constructor(config: PiQueryAgentConfig) {
|
||||||
this.sessionManager = config.sessionManager
|
this.sessionManager = config.sessionManager
|
||||||
@@ -48,6 +54,7 @@ export class PiQueryAgent implements QueryAgent {
|
|||||||
this.apiKey = config.apiKey
|
this.apiKey = config.apiKey
|
||||||
this.cwd = config.cwd ?? tmpdir()
|
this.cwd = config.cwd ?? tmpdir()
|
||||||
this.systemPrompt = config.systemPrompt ?? defaultSystemPrompt
|
this.systemPrompt = config.systemPrompt ?? defaultSystemPrompt
|
||||||
|
this.clock = config.clock ?? (() => new Date())
|
||||||
}
|
}
|
||||||
|
|
||||||
async *ask(input: QueryAgentAsk): AsyncIterable<QueryAgentEvent> {
|
async *ask(input: QueryAgentAsk): AsyncIterable<QueryAgentEvent> {
|
||||||
@@ -59,7 +66,7 @@ export class PiQueryAgent implements QueryAgent {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const run = Symbol(input.userId)
|
const run: ActiveRun = { proposedActions: [] }
|
||||||
this.activeRuns.set(input.userId, run)
|
this.activeRuns.set(input.userId, run)
|
||||||
|
|
||||||
let session: PiSession
|
let session: PiSession
|
||||||
@@ -110,6 +117,9 @@ export class PiQueryAgent implements QueryAgent {
|
|||||||
void this.runPrompt(session, input)
|
void this.runPrompt(session, input)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (runFailed) return
|
if (runFailed) return
|
||||||
|
for (const action of run.proposedActions) {
|
||||||
|
pushRunEvent({ type: "action_proposed", action })
|
||||||
|
}
|
||||||
pushRunEvent({ type: "done" })
|
pushRunEvent({ type: "done" })
|
||||||
})
|
})
|
||||||
.catch((err: unknown) => {
|
.catch((err: unknown) => {
|
||||||
@@ -151,7 +161,7 @@ export class PiQueryAgent implements QueryAgent {
|
|||||||
this.activeRuns.clear()
|
this.activeRuns.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearActiveRun(userId: string, run: symbol): void {
|
private clearActiveRun(userId: string, run: ActiveRun): void {
|
||||||
if (this.activeRuns.get(userId) === run) {
|
if (this.activeRuns.get(userId) === run) {
|
||||||
this.activeRuns.delete(userId)
|
this.activeRuns.delete(userId)
|
||||||
}
|
}
|
||||||
@@ -204,6 +214,10 @@ export class PiQueryAgent implements QueryAgent {
|
|||||||
customTools: createFreyaAgentTools({
|
customTools: createFreyaAgentTools({
|
||||||
userId,
|
userId,
|
||||||
sessionManager: this.sessionManager,
|
sessionManager: this.sessionManager,
|
||||||
|
clock: this.clock,
|
||||||
|
proposeAction: (action) => {
|
||||||
|
this.activeRuns.get(userId)?.proposedActions.push(action)
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
tools: [...FREYA_AGENT_TOOL_NAMES],
|
tools: [...FREYA_AGENT_TOOL_NAMES],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
<identity>
|
<identity>
|
||||||
You are Freya. You are a digital companion created by Kenneth. His twitter is @kennethnym.
|
You are Freya. You are a digital companion created by Kenneth. His twitter is @kennethnym.
|
||||||
You have access to user data via the context graph. It stores the latest snapshot of all user data and context.
|
|
||||||
It reactively updates based on external events, such as, but not exclusively, when the user moves, when a new email arrives, when weather updates are available, and when transit alerts are issued.
|
|
||||||
</identity>
|
</identity>
|
||||||
|
|
||||||
<action>
|
<action>
|
||||||
@@ -17,9 +15,9 @@ freya_list_context: when you need to inspect all current context graph entries.
|
|||||||
|
|
||||||
freya_get_source_data: when you need current feed items, context entries, actions, or errors for a specific source ID.
|
freya_get_source_data: when you need current feed items, context entries, actions, or errors for a specific source ID.
|
||||||
|
|
||||||
freya_execute_action: when the user asks you to perform an available source action, or when the source action is non-mutating and tool-like. This executes immediately.
|
freya_propose_action: when the user asks to change state or when you recommend a concrete action that should be confirmed first. This tool only proposes an action. It does not execute the action.
|
||||||
|
|
||||||
If you need more information to answer user's query, call freya_execute_action with sourceId "freya.web-search", actionId "search", and params containing query and numResults, for example {"query":"latest relevant information","numResults":5}.
|
if you need more information to answer user's query, call freya_propose_action with freya.web-search source id.
|
||||||
</action>
|
</action>
|
||||||
|
|
||||||
<behavior>
|
<behavior>
|
||||||
|
|||||||
@@ -3,10 +3,22 @@ export interface QueryAgentAsk {
|
|||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProposedAction {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
sourceId?: string
|
||||||
|
actionId?: string
|
||||||
|
params?: unknown
|
||||||
|
requiresConfirmation: true
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
export type QueryAgentEvent =
|
export type QueryAgentEvent =
|
||||||
| { type: "text_delta"; text: string }
|
| { type: "text_delta"; text: string }
|
||||||
| { type: "tool_start"; toolName: string }
|
| { type: "tool_start"; toolName: string }
|
||||||
| { type: "tool_end"; toolName: string; ok: boolean }
|
| { type: "tool_end"; toolName: string; ok: boolean }
|
||||||
|
| { type: "action_proposed"; action: ProposedAction }
|
||||||
| { type: "done" }
|
| { type: "done" }
|
||||||
| { type: "error"; message: string }
|
| { type: "error"; message: string }
|
||||||
|
|
||||||
@@ -18,6 +30,7 @@ export interface QueryAgent {
|
|||||||
|
|
||||||
export interface QueryAgentResponse {
|
export interface QueryAgentResponse {
|
||||||
message: string
|
message: string
|
||||||
|
proposedActions: ProposedAction[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export class QueryAgentError extends Error {
|
export class QueryAgentError extends Error {
|
||||||
@@ -32,12 +45,16 @@ export async function collectQueryAgentResponse(
|
|||||||
input: QueryAgentAsk,
|
input: QueryAgentAsk,
|
||||||
): Promise<QueryAgentResponse> {
|
): Promise<QueryAgentResponse> {
|
||||||
let message = ""
|
let message = ""
|
||||||
|
const proposedActions: ProposedAction[] = []
|
||||||
|
|
||||||
for await (const event of agent.ask(input)) {
|
for await (const event of agent.ask(input)) {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case "text_delta":
|
case "text_delta":
|
||||||
message += event.text
|
message += event.text
|
||||||
break
|
break
|
||||||
|
case "action_proposed":
|
||||||
|
proposedActions.push(event.action)
|
||||||
|
break
|
||||||
case "error":
|
case "error":
|
||||||
throw new QueryAgentError(event.message)
|
throw new QueryAgentError(event.message)
|
||||||
case "tool_start":
|
case "tool_start":
|
||||||
@@ -47,5 +64,5 @@ export async function collectQueryAgentResponse(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { message }
|
return { message, proposedActions }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,15 @@ import { Type } from "typebox"
|
|||||||
|
|
||||||
import type { UserSessionManager } from "../session/index.ts"
|
import type { UserSessionManager } from "../session/index.ts"
|
||||||
import type { QueryDebugTools } from "./debug-tools.ts"
|
import type { QueryDebugTools } from "./debug-tools.ts"
|
||||||
|
import type { ProposedAction } from "./query-agent.ts"
|
||||||
|
|
||||||
import { createQueryDebugTools } from "./debug-tools.ts"
|
import { createQueryDebugTools } from "./debug-tools.ts"
|
||||||
|
|
||||||
interface CreateFreyaAgentToolsConfig {
|
interface CreateFreyaAgentToolsConfig {
|
||||||
userId: string
|
userId: string
|
||||||
sessionManager: UserSessionManager
|
sessionManager: UserSessionManager
|
||||||
|
clock: () => Date
|
||||||
|
proposeAction(action: ProposedAction): void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FREYA_QUERY_CONTEXT_TOOL = "freya_query_context"
|
export const FREYA_QUERY_CONTEXT_TOOL = "freya_query_context"
|
||||||
@@ -17,7 +20,7 @@ export const FREYA_GET_CONTEXT_TOOL = "freya_get_context"
|
|||||||
export const FREYA_LIST_CONTEXT_TOOL = "freya_list_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_SOURCE_DATA_TOOL = "freya_get_source_data"
|
||||||
export const FREYA_GET_FEED_ITEM_TOOL = "freya_get_feed_item"
|
export const FREYA_GET_FEED_ITEM_TOOL = "freya_get_feed_item"
|
||||||
export const FREYA_EXECUTE_ACTION_TOOL = "freya_execute_action"
|
export const FREYA_PROPOSE_ACTION_TOOL = "freya_propose_action"
|
||||||
|
|
||||||
export const FREYA_AGENT_TOOL_NAMES = [
|
export const FREYA_AGENT_TOOL_NAMES = [
|
||||||
FREYA_LIST_SOURCES_TOOL,
|
FREYA_LIST_SOURCES_TOOL,
|
||||||
@@ -26,7 +29,7 @@ export const FREYA_AGENT_TOOL_NAMES = [
|
|||||||
FREYA_QUERY_CONTEXT_TOOL,
|
FREYA_QUERY_CONTEXT_TOOL,
|
||||||
FREYA_LIST_CONTEXT_TOOL,
|
FREYA_LIST_CONTEXT_TOOL,
|
||||||
FREYA_GET_SOURCE_DATA_TOOL,
|
FREYA_GET_SOURCE_DATA_TOOL,
|
||||||
FREYA_EXECUTE_ACTION_TOOL,
|
FREYA_PROPOSE_ACTION_TOOL,
|
||||||
]
|
]
|
||||||
|
|
||||||
export function createFreyaAgentTools(config: CreateFreyaAgentToolsConfig) {
|
export function createFreyaAgentTools(config: CreateFreyaAgentToolsConfig) {
|
||||||
@@ -118,21 +121,28 @@ export function createFreyaAgentTools(config: CreateFreyaAgentToolsConfig) {
|
|||||||
execute: async (_toolCallId, params) => executeGetSourceDataTool(config, params),
|
execute: async (_toolCallId, params) => executeGetSourceDataTool(config, params),
|
||||||
})
|
})
|
||||||
|
|
||||||
const executeActionTool = defineTool({
|
const proposeActionTool = defineTool({
|
||||||
name: FREYA_EXECUTE_ACTION_TOOL,
|
name: FREYA_PROPOSE_ACTION_TOOL,
|
||||||
label: "Execute FREYA Action",
|
label: "Propose FREYA Action",
|
||||||
description:
|
description: "Create a proposed action for the user to review. This never executes the action.",
|
||||||
"Execute an available FREYA source action immediately without creating a proposal.",
|
|
||||||
parameters: Type.Object({
|
parameters: Type.Object({
|
||||||
sourceId: Type.String({ description: "Source ID that should execute the action." }),
|
title: Type.String({ description: "Short user-facing action title." }),
|
||||||
actionId: Type.String({ description: "Source action ID to execute." }),
|
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(
|
params: Type.Optional(
|
||||||
Type.Unknown({
|
Type.Unknown({
|
||||||
description: "Parameters to pass to the source action.",
|
description: "Parameters to pass to the source action after confirmation.",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
execute: async (_toolCallId, params) => executeActionToolCall(config, params),
|
execute: async (_toolCallId, params) => executeProposeActionTool(config, params),
|
||||||
})
|
})
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -142,7 +152,7 @@ export function createFreyaAgentTools(config: CreateFreyaAgentToolsConfig) {
|
|||||||
queryContextTool,
|
queryContextTool,
|
||||||
listContextTool,
|
listContextTool,
|
||||||
getSourceDataTool,
|
getSourceDataTool,
|
||||||
executeActionTool,
|
proposeActionTool,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,37 +285,40 @@ async function executeGetSourceDataTool(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeActionToolCall(
|
function executeProposeActionTool(
|
||||||
config: CreateFreyaAgentToolsConfig,
|
config: CreateFreyaAgentToolsConfig,
|
||||||
params: {
|
params: {
|
||||||
sourceId: string
|
title: string
|
||||||
actionId: string
|
description: string
|
||||||
|
sourceId?: string
|
||||||
|
actionId?: string
|
||||||
params?: unknown
|
params?: unknown
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const userSession = await config.sessionManager.getOrCreate(config.userId)
|
const action: ProposedAction = {
|
||||||
const result = await userSession.engine.executeAction(
|
id: crypto.randomUUID(),
|
||||||
params.sourceId,
|
title: params.title,
|
||||||
params.actionId,
|
description: params.description,
|
||||||
params.params,
|
requiresConfirmation: true,
|
||||||
)
|
createdAt: config.clock().toISOString(),
|
||||||
|
...(params.sourceId ? { sourceId: params.sourceId } : {}),
|
||||||
const actionExecution = {
|
...(params.actionId ? { actionId: params.actionId } : {}),
|
||||||
sourceId: params.sourceId,
|
...(params.params !== undefined ? { params: params.params } : {}),
|
||||||
actionId: params.actionId,
|
|
||||||
result: result ?? null,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.proposeAction(action)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text" as const,
|
type: "text" as const,
|
||||||
text: JSON.stringify({
|
text: JSON.stringify({
|
||||||
ok: true,
|
ok: true,
|
||||||
...actionExecution,
|
proposedActionId: action.id,
|
||||||
|
requiresConfirmation: true,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
details: { actionExecution },
|
details: { proposedAction: action },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { LocationSource } from "@freya/source-location"
|
import { LocationSource } from "@freya/source-location"
|
||||||
import { ReminderSource } from "@freya/source-reminders"
|
|
||||||
import { WebSearchSource } from "@freya/source-web-search"
|
import { WebSearchSource } from "@freya/source-web-search"
|
||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
|
|
||||||
@@ -56,12 +55,8 @@ function createRecordingDb(): RecordingDb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("default user sources", () => {
|
describe("default user sources", () => {
|
||||||
test("defines default enabled sources", () => {
|
test("defines location and web search as default enabled sources", () => {
|
||||||
expect(DEFAULT_ENABLED_SOURCE_IDS).toEqual([
|
expect(DEFAULT_ENABLED_SOURCE_IDS).toEqual([LocationSource.id, WebSearchSource.id])
|
||||||
LocationSource.id,
|
|
||||||
ReminderSource.id,
|
|
||||||
WebSearchSource.id,
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("inserts default enabled source rows for a user", async () => {
|
test("inserts default enabled source rows for a user", async () => {
|
||||||
@@ -75,7 +70,7 @@ describe("default user sources", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expect(recording.table()).toBe(userSources)
|
expect(recording.table()).toBe(userSources)
|
||||||
expect(rows).toHaveLength(3)
|
expect(rows).toHaveLength(2)
|
||||||
expect(rows.map((row) => row.sourceId)).toEqual([...DEFAULT_ENABLED_SOURCE_IDS])
|
expect(rows.map((row) => row.sourceId)).toEqual([...DEFAULT_ENABLED_SOURCE_IDS])
|
||||||
expect(recording.conflictTarget()).toEqual([userSources.userId, userSources.sourceId])
|
expect(recording.conflictTarget()).toEqual([userSources.userId, userSources.sourceId])
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
import { LocationSource } from "@freya/source-location"
|
import { LocationSource } from "@freya/source-location"
|
||||||
import { ReminderSource } from "@freya/source-reminders"
|
|
||||||
import { WebSearchSource } from "@freya/source-web-search"
|
import { WebSearchSource } from "@freya/source-web-search"
|
||||||
|
|
||||||
import type { Database } from "../db/index.ts"
|
import type { Database } from "../db/index.ts"
|
||||||
|
|
||||||
import { userSources } from "../db/schema.ts"
|
import { userSources } from "../db/schema.ts"
|
||||||
|
|
||||||
export const DEFAULT_ENABLED_SOURCE_IDS = [
|
export const DEFAULT_ENABLED_SOURCE_IDS = [LocationSource.id, WebSearchSource.id] as const
|
||||||
LocationSource.id,
|
|
||||||
ReminderSource.id,
|
|
||||||
WebSearchSource.id,
|
|
||||||
] as const
|
|
||||||
|
|
||||||
export type DefaultEnabledSourceId = (typeof DEFAULT_ENABLED_SOURCE_IDS)[number]
|
export type DefaultEnabledSourceId = (typeof DEFAULT_ENABLED_SOURCE_IDS)[number]
|
||||||
|
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ export function meta({}: Route.MetaArgs) {
|
|||||||
},
|
},
|
||||||
{ property: "og:title", content: PAGE_TITLE },
|
{ property: "og:title", content: PAGE_TITLE },
|
||||||
{ property: "og:description", content: PAGE_DESCRIPTION },
|
{ property: "og:description", content: PAGE_DESCRIPTION },
|
||||||
{ property: "og:image", content: "https://freya.chat/social-media-preview.jpg" },
|
{ property: "og:image", content: "https://ael.is/social-media-preview.png" },
|
||||||
{ property: "og:url", content: "https://freya.chat" },
|
{ property: "og:url", content: "https://ael.is" },
|
||||||
{ property: "og:type", content: "website" },
|
{ property: "og:type", content: "website" },
|
||||||
{ name: "twitter:card", content: "summary_large_image" },
|
{ name: "twitter:card", content: "summary_large_image" },
|
||||||
{ name: "twitter:title", content: PAGE_TITLE },
|
{ name: "twitter:title", content: PAGE_TITLE },
|
||||||
{ name: "twitter:description", content: PAGE_DESCRIPTION },
|
{ name: "twitter:description", content: PAGE_DESCRIPTION },
|
||||||
{ name: "twitter:image", content: "https://freya.chat/social-media-preview.jpg" },
|
{ name: "twitter:image", content: "https://ael.is/social-media-preview.png" },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ export async function action({ request }: Route.ActionArgs) {
|
|||||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
|
|
||||||
const emailRes = await resend.emails.send({
|
const emailRes = await resend.emails.send({
|
||||||
from: "Freya <no-reply@freya.chat>",
|
from: "Freya <no-reply@ael.is>",
|
||||||
to: email,
|
to: email,
|
||||||
template: {
|
template: {
|
||||||
id: "waitlist-confirmation",
|
id: "waitlist-confirmation",
|
||||||
@@ -380,6 +380,7 @@ function SystemMessageBubble({
|
|||||||
isAnimating={isStreaming}
|
isAnimating={isStreaming}
|
||||||
linkSafety={{ enabled: false }}
|
linkSafety={{ enabled: false }}
|
||||||
components={{
|
components={{
|
||||||
|
// @ts-expect-error
|
||||||
a: ({ className, ...props }) => <a className={`underline ${className}`} {...props} />,
|
a: ({ className, ...props }) => <a className={`underline ${className}`} {...props} />,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ const POLICY = `# Privacy Policy
|
|||||||
|
|
||||||
**Last updated:** March 5, 2026
|
**Last updated:** March 5, 2026
|
||||||
|
|
||||||
This Privacy Policy describes how **Freya** ("we", "us", or "our") collects, uses, and protects your personal information when you visit **https://freya.chat** or interact with our services.
|
This Privacy Policy describes how **Freya** ("we", "us", or "our") collects, uses, and protects your personal information when you visit **https://ael.is** or interact with our services.
|
||||||
|
|
||||||
If you do not agree with this Privacy Policy, please do not use the website.
|
If you do not agree with this Privacy Policy, please do not use the website.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
User-agent: *
|
User-agent: *
|
||||||
Allow: /
|
Allow: /
|
||||||
|
|
||||||
Sitemap: https://freya.chat/sitemap.xml
|
Sitemap: https://ael.is/sitemap.xml
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
<url>
|
<url>
|
||||||
<loc>https://freya.chat/</loc>
|
<loc>https://ael.is/</loc>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://freya.chat/privacy</loc>
|
<loc>https://ael.is/privacy</loc>
|
||||||
</url>
|
</url>
|
||||||
</urlset>
|
</urlset>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 45 KiB |
BIN
apps/waitlist-website/public/social-media-preview.png
Normal file
BIN
apps/waitlist-website/public/social-media-preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
@@ -84,9 +84,7 @@ const ONE_DAY_MS = 24 * 60 * 60 * 1000
|
|||||||
* It owns recurrence expansion, edit-scope semantics, and feed item signals.
|
* It owns recurrence expansion, edit-scope semantics, and feed item signals.
|
||||||
*/
|
*/
|
||||||
export class ReminderSource implements FeedSource<ReminderFeedItem> {
|
export class ReminderSource implements FeedSource<ReminderFeedItem> {
|
||||||
static readonly id = "freya.reminders"
|
readonly id = "freya.reminders"
|
||||||
|
|
||||||
readonly id = ReminderSource.id
|
|
||||||
|
|
||||||
private readonly storage: ReminderStorage
|
private readonly storage: ReminderStorage
|
||||||
private readonly lookAheadMs: number
|
private readonly lookAheadMs: number
|
||||||
|
|||||||
Reference in New Issue
Block a user