mirror of
https://github.com/kennethnym/aris.git
synced 2026-06-15 03:51: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 {
|
||||
message: string
|
||||
proposedActions: ProposedAction[]
|
||||
}
|
||||
|
||||
interface QueryToolDefinition {
|
||||
@@ -175,6 +187,7 @@ async function askAgent(backendUrl: string, cookies: CookieJar, message: string)
|
||||
}
|
||||
|
||||
console.log(`\nagent> ${data.message || "(no message)"}`)
|
||||
printProposedActions(data.proposedActions)
|
||||
console.log("")
|
||||
}
|
||||
|
||||
@@ -353,6 +366,22 @@ function printHelp(): void {
|
||||
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(
|
||||
label: string,
|
||||
defaultValue?: string,
|
||||
@@ -550,7 +579,9 @@ function isAuthSession(value: unknown): value is AuthSession {
|
||||
|
||||
function isQueryResponse(value: unknown): value is QueryResponse {
|
||||
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 {
|
||||
@@ -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 {
|
||||
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() {
|
||||
@@ -131,16 +109,6 @@ function createTestDebugTools() {
|
||||
async listActions(sourceId: string) {
|
||||
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) {
|
||||
return sourceId in actions
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { contextKey, type ContextKeyPart } from "@freya/core"
|
||||
|
||||
import type { UserSessionManager } from "../session/index.ts"
|
||||
import type { ProposedAction } from "./query-agent.ts"
|
||||
|
||||
type ToolParams = Record<string, unknown>
|
||||
|
||||
@@ -22,7 +23,7 @@ const FreyaGetContextTool = "freya_get_context"
|
||||
const FreyaListContextTool = "freya_list_context"
|
||||
const FreyaGetSourceDataTool = "freya_get_source_data"
|
||||
const FreyaGetFeedItemTool = "freya_get_feed_item"
|
||||
const FreyaExecuteActionTool = "freya_execute_action"
|
||||
const FreyaProposeActionTool = "freya_propose_action"
|
||||
|
||||
export function createQueryDebugTools(sessionManager: UserSessionManager): QueryDebugTools {
|
||||
return new DefaultQueryDebugTools(sessionManager)
|
||||
@@ -85,12 +86,14 @@ class DefaultQueryDebugTools implements QueryDebugTools {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: FreyaExecuteActionTool,
|
||||
label: "Execute FREYA Action",
|
||||
description: "Execute an available source action immediately.",
|
||||
name: FreyaProposeActionTool,
|
||||
label: "Propose FREYA Action",
|
||||
description: "Create a proposed action object without executing it.",
|
||||
parameters: {
|
||||
sourceId: "string",
|
||||
actionId: "string",
|
||||
title: "string",
|
||||
description: "string",
|
||||
sourceId: "string?",
|
||||
actionId: "string?",
|
||||
params: "unknown?",
|
||||
},
|
||||
},
|
||||
@@ -111,8 +114,8 @@ class DefaultQueryDebugTools implements QueryDebugTools {
|
||||
return this.listContext(userId)
|
||||
case FreyaGetSourceDataTool:
|
||||
return this.getSourceData(userId, expectToolParams(params, ["sourceId"]))
|
||||
case FreyaExecuteActionTool:
|
||||
return this.executeAction(userId, expectToolParams(params, ["sourceId", "actionId"]))
|
||||
case FreyaProposeActionTool:
|
||||
return proposeAction(expectToolParams(params, ["title", "description"]))
|
||||
default:
|
||||
throw new Error(`Unknown debug tool: ${toolName}`)
|
||||
}
|
||||
@@ -319,20 +322,27 @@ class DefaultQueryDebugTools implements QueryDebugTools {
|
||||
errors,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async executeAction(userId: string, params: ToolParams): Promise<unknown> {
|
||||
const sourceId = expectString(params, "sourceId")
|
||||
const actionId = expectString(params, "actionId")
|
||||
const actionParams = "params" in params ? params.params : undefined
|
||||
const userSession = await this.sessionManager.getOrCreate(userId)
|
||||
const result = await userSession.engine.executeAction(sourceId, actionId, actionParams)
|
||||
function proposeAction(params: ToolParams): unknown {
|
||||
const sourceId = optionalString(params, "sourceId")
|
||||
const actionId = optionalString(params, "actionId")
|
||||
const action: ProposedAction = {
|
||||
id: crypto.randomUUID(),
|
||||
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 {
|
||||
ok: true,
|
||||
sourceId,
|
||||
actionId,
|
||||
result: result ?? null,
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
proposedActionId: action.id,
|
||||
requiresConfirmation: true,
|
||||
proposedAction: action,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"
|
||||
import { Hono } from "hono"
|
||||
|
||||
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 { registerAgentHttpHandlers, registerDebugAgentHttpHandlers } from "./http.ts"
|
||||
@@ -80,10 +80,21 @@ describe("POST /api/agent", () => {
|
||||
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([
|
||||
{ type: "text_delta", text: "You should " },
|
||||
{ type: "text_delta", text: "leave at 8:30." },
|
||||
{ type: "action_proposed", action },
|
||||
{ type: "done" },
|
||||
])
|
||||
const app = buildTestApp(agent, "user-1")
|
||||
@@ -101,8 +112,10 @@ describe("POST /api/agent", () => {
|
||||
|
||||
const body = (await res.json()) as {
|
||||
message: string
|
||||
proposedActions: ProposedAction[]
|
||||
}
|
||||
expect(body.message).toBe("You should leave at 8:30.")
|
||||
expect(body.proposedActions).toEqual([action])
|
||||
})
|
||||
|
||||
test("returns 400 for invalid body", async () => {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import { tmpdir } from "node:os"
|
||||
|
||||
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 defaultSystemPrompt from "./prompts/system.txt"
|
||||
@@ -28,18 +28,24 @@ export interface PiQueryAgentConfig {
|
||||
apiKey?: string
|
||||
cwd?: string
|
||||
systemPrompt?: string
|
||||
clock?: () => Date
|
||||
}
|
||||
|
||||
interface ActiveRun {
|
||||
proposedActions: ProposedAction[]
|
||||
}
|
||||
|
||||
export class PiQueryAgent implements QueryAgent {
|
||||
private readonly sessionManager: UserSessionManager
|
||||
private readonly cwd: string
|
||||
private readonly systemPrompt: string
|
||||
private readonly clock: () => Date
|
||||
private readonly modelProvider: string
|
||||
private readonly modelId: string
|
||||
private readonly apiKey: string | undefined
|
||||
private readonly sessions = new Map<string, 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) {
|
||||
this.sessionManager = config.sessionManager
|
||||
@@ -48,6 +54,7 @@ export class PiQueryAgent implements QueryAgent {
|
||||
this.apiKey = config.apiKey
|
||||
this.cwd = config.cwd ?? tmpdir()
|
||||
this.systemPrompt = config.systemPrompt ?? defaultSystemPrompt
|
||||
this.clock = config.clock ?? (() => new Date())
|
||||
}
|
||||
|
||||
async *ask(input: QueryAgentAsk): AsyncIterable<QueryAgentEvent> {
|
||||
@@ -59,7 +66,7 @@ export class PiQueryAgent implements QueryAgent {
|
||||
return
|
||||
}
|
||||
|
||||
const run = Symbol(input.userId)
|
||||
const run: ActiveRun = { proposedActions: [] }
|
||||
this.activeRuns.set(input.userId, run)
|
||||
|
||||
let session: PiSession
|
||||
@@ -110,6 +117,9 @@ export class PiQueryAgent implements QueryAgent {
|
||||
void this.runPrompt(session, input)
|
||||
.then(() => {
|
||||
if (runFailed) return
|
||||
for (const action of run.proposedActions) {
|
||||
pushRunEvent({ type: "action_proposed", action })
|
||||
}
|
||||
pushRunEvent({ type: "done" })
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
@@ -151,7 +161,7 @@ export class PiQueryAgent implements QueryAgent {
|
||||
this.activeRuns.clear()
|
||||
}
|
||||
|
||||
private clearActiveRun(userId: string, run: symbol): void {
|
||||
private clearActiveRun(userId: string, run: ActiveRun): void {
|
||||
if (this.activeRuns.get(userId) === run) {
|
||||
this.activeRuns.delete(userId)
|
||||
}
|
||||
@@ -204,6 +214,10 @@ export class PiQueryAgent implements QueryAgent {
|
||||
customTools: createFreyaAgentTools({
|
||||
userId,
|
||||
sessionManager: this.sessionManager,
|
||||
clock: this.clock,
|
||||
proposeAction: (action) => {
|
||||
this.activeRuns.get(userId)?.proposedActions.push(action)
|
||||
},
|
||||
}),
|
||||
tools: [...FREYA_AGENT_TOOL_NAMES],
|
||||
})
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<identity>
|
||||
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>
|
||||
|
||||
<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_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>
|
||||
|
||||
<behavior>
|
||||
|
||||
@@ -3,10 +3,22 @@ export interface QueryAgentAsk {
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface ProposedAction {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
sourceId?: string
|
||||
actionId?: string
|
||||
params?: unknown
|
||||
requiresConfirmation: true
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export type QueryAgentEvent =
|
||||
| { type: "text_delta"; text: string }
|
||||
| { type: "tool_start"; toolName: string }
|
||||
| { type: "tool_end"; toolName: string; ok: boolean }
|
||||
| { type: "action_proposed"; action: ProposedAction }
|
||||
| { type: "done" }
|
||||
| { type: "error"; message: string }
|
||||
|
||||
@@ -18,6 +30,7 @@ export interface QueryAgent {
|
||||
|
||||
export interface QueryAgentResponse {
|
||||
message: string
|
||||
proposedActions: ProposedAction[]
|
||||
}
|
||||
|
||||
export class QueryAgentError extends Error {
|
||||
@@ -32,12 +45,16 @@ export async function collectQueryAgentResponse(
|
||||
input: QueryAgentAsk,
|
||||
): Promise<QueryAgentResponse> {
|
||||
let message = ""
|
||||
const proposedActions: ProposedAction[] = []
|
||||
|
||||
for await (const event of agent.ask(input)) {
|
||||
switch (event.type) {
|
||||
case "text_delta":
|
||||
message += event.text
|
||||
break
|
||||
case "action_proposed":
|
||||
proposedActions.push(event.action)
|
||||
break
|
||||
case "error":
|
||||
throw new QueryAgentError(event.message)
|
||||
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 { 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"
|
||||
@@ -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_GET_SOURCE_DATA_TOOL = "freya_get_source_data"
|
||||
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 = [
|
||||
FREYA_LIST_SOURCES_TOOL,
|
||||
@@ -26,7 +29,7 @@ export const FREYA_AGENT_TOOL_NAMES = [
|
||||
FREYA_QUERY_CONTEXT_TOOL,
|
||||
FREYA_LIST_CONTEXT_TOOL,
|
||||
FREYA_GET_SOURCE_DATA_TOOL,
|
||||
FREYA_EXECUTE_ACTION_TOOL,
|
||||
FREYA_PROPOSE_ACTION_TOOL,
|
||||
]
|
||||
|
||||
export function createFreyaAgentTools(config: CreateFreyaAgentToolsConfig) {
|
||||
@@ -118,21 +121,28 @@ export function createFreyaAgentTools(config: CreateFreyaAgentToolsConfig) {
|
||||
execute: async (_toolCallId, params) => executeGetSourceDataTool(config, params),
|
||||
})
|
||||
|
||||
const executeActionTool = defineTool({
|
||||
name: FREYA_EXECUTE_ACTION_TOOL,
|
||||
label: "Execute FREYA Action",
|
||||
description:
|
||||
"Execute an available FREYA source action immediately without creating a proposal.",
|
||||
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({
|
||||
sourceId: Type.String({ description: "Source ID that should execute the action." }),
|
||||
actionId: Type.String({ description: "Source action ID to execute." }),
|
||||
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.",
|
||||
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 [
|
||||
@@ -142,7 +152,7 @@ export function createFreyaAgentTools(config: CreateFreyaAgentToolsConfig) {
|
||||
queryContextTool,
|
||||
listContextTool,
|
||||
getSourceDataTool,
|
||||
executeActionTool,
|
||||
proposeActionTool,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -275,37 +285,40 @@ async function executeGetSourceDataTool(
|
||||
}
|
||||
}
|
||||
|
||||
async function executeActionToolCall(
|
||||
function executeProposeActionTool(
|
||||
config: CreateFreyaAgentToolsConfig,
|
||||
params: {
|
||||
sourceId: string
|
||||
actionId: string
|
||||
title: string
|
||||
description: string
|
||||
sourceId?: string
|
||||
actionId?: string
|
||||
params?: unknown
|
||||
},
|
||||
) {
|
||||
const userSession = await config.sessionManager.getOrCreate(config.userId)
|
||||
const result = await userSession.engine.executeAction(
|
||||
params.sourceId,
|
||||
params.actionId,
|
||||
params.params,
|
||||
)
|
||||
|
||||
const actionExecution = {
|
||||
sourceId: params.sourceId,
|
||||
actionId: params.actionId,
|
||||
result: result ?? null,
|
||||
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,
|
||||
...actionExecution,
|
||||
proposedActionId: action.id,
|
||||
requiresConfirmation: true,
|
||||
}),
|
||||
},
|
||||
],
|
||||
details: { actionExecution },
|
||||
details: { proposedAction: action },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { LocationSource } from "@freya/source-location"
|
||||
import { ReminderSource } from "@freya/source-reminders"
|
||||
import { WebSearchSource } from "@freya/source-web-search"
|
||||
import { describe, expect, test } from "bun:test"
|
||||
|
||||
@@ -56,12 +55,8 @@ function createRecordingDb(): RecordingDb {
|
||||
}
|
||||
|
||||
describe("default user sources", () => {
|
||||
test("defines default enabled sources", () => {
|
||||
expect(DEFAULT_ENABLED_SOURCE_IDS).toEqual([
|
||||
LocationSource.id,
|
||||
ReminderSource.id,
|
||||
WebSearchSource.id,
|
||||
])
|
||||
test("defines location and web search as default enabled sources", () => {
|
||||
expect(DEFAULT_ENABLED_SOURCE_IDS).toEqual([LocationSource.id, WebSearchSource.id])
|
||||
})
|
||||
|
||||
test("inserts default enabled source rows for a user", async () => {
|
||||
@@ -75,7 +70,7 @@ describe("default user sources", () => {
|
||||
}
|
||||
|
||||
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(recording.conflictTarget()).toEqual([userSources.userId, userSources.sourceId])
|
||||
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import { LocationSource } from "@freya/source-location"
|
||||
import { ReminderSource } from "@freya/source-reminders"
|
||||
import { WebSearchSource } from "@freya/source-web-search"
|
||||
|
||||
import type { Database } from "../db/index.ts"
|
||||
|
||||
import { userSources } from "../db/schema.ts"
|
||||
|
||||
export const DEFAULT_ENABLED_SOURCE_IDS = [
|
||||
LocationSource.id,
|
||||
ReminderSource.id,
|
||||
WebSearchSource.id,
|
||||
] as const
|
||||
export const DEFAULT_ENABLED_SOURCE_IDS = [LocationSource.id, WebSearchSource.id] as const
|
||||
|
||||
export type DefaultEnabledSourceId = (typeof DEFAULT_ENABLED_SOURCE_IDS)[number]
|
||||
|
||||
|
||||
@@ -84,9 +84,7 @@ const ONE_DAY_MS = 24 * 60 * 60 * 1000
|
||||
* It owns recurrence expansion, edit-scope semantics, and feed item signals.
|
||||
*/
|
||||
export class ReminderSource implements FeedSource<ReminderFeedItem> {
|
||||
static readonly id = "freya.reminders"
|
||||
|
||||
readonly id = ReminderSource.id
|
||||
readonly id = "freya.reminders"
|
||||
|
||||
private readonly storage: ReminderStorage
|
||||
private readonly lookAheadMs: number
|
||||
|
||||
Reference in New Issue
Block a user