Compare commits

..

1 Commits

Author SHA1 Message Date
61d2245261 build: update lockfile 2026-06-14 19:22:43 +01:00
11 changed files with 175 additions and 109 deletions

View File

@@ -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)
} }

View File

@@ -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

View File

@@ -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,
}
} }
} }

View File

@@ -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 () => {

View File

@@ -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],
}) })

View File

@@ -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>

View File

@@ -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 }
} }

View File

@@ -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 },
} }
} }

View File

@@ -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])

View File

@@ -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]

View File

@@ -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