mirror of
https://github.com/runebookai/tome.git
synced 2025-07-21 00:27:30 +03:00
Support ephemeral Sessions
This commit is contained in:
@@ -250,6 +250,10 @@ CREATE TABLE IF NOT EXISTS tasks (
|
|||||||
version: 15,
|
version: 15,
|
||||||
description: "add_tasks_support",
|
description: "add_tasks_support",
|
||||||
sql: r#"
|
sql: r#"
|
||||||
|
ALTER TABLE sessions ADD COLUMN ephemeral BOOLEAN DEFAULT "false";
|
||||||
|
|
||||||
|
INSERT INTO apps ("name", "description", "interface") VALUES ("Task", "Scheduled task", "Task");
|
||||||
|
|
||||||
ALTER TABLE tasks ADD COLUMN engine_id INTEGER REFERENCES engines(id);
|
ALTER TABLE tasks ADD COLUMN engine_id INTEGER REFERENCES engines(id);
|
||||||
ALTER TABLE tasks ADD COLUMN model TEXT NOT NULL;
|
ALTER TABLE tasks ADD COLUMN model TEXT NOT NULL;
|
||||||
|
|
||||||
@@ -257,8 +261,8 @@ CREATE TABLE IF NOT EXISTS task_runs (
|
|||||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
task_id INTEGER NOT NULL,
|
task_id INTEGER NOT NULL,
|
||||||
session_id INTEGER NOT NULL,
|
session_id INTEGER NOT NULL,
|
||||||
success BOOLEAN NOT NULL,
|
state TEXT NOT NULL DEFAULT "Pending",
|
||||||
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY(task_id) REFERENCES tasks(id),
|
FOREIGN KEY(task_id) REFERENCES tasks(id),
|
||||||
FOREIGN KEY(session_id) REFERENCES sessions(id)
|
FOREIGN KEY(session_id) REFERENCES sessions(id)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Flex class={twMerge('w-full flex-col items-start', cls?.toString())}>
|
<Flex class={twMerge('w-full flex-col items-start overflow-y-scroll', cls?.toString())}>
|
||||||
{#if title}
|
{#if title}
|
||||||
<p class={twMerge('text-medium text-sm', titleClass?.toString())}>
|
<p class={twMerge('text-medium text-sm', titleClass?.toString())}>
|
||||||
{title}
|
{title}
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
const { class: cls = '' } = $props();
|
const { class: cls = 'w-[24px] h-[24px]' } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={twMerge('spinner', cls?.toString())}></div>
|
<div class={twMerge('spinner', cls?.toString())}></div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.spinner {
|
.spinner {
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
position: relative;
|
position: relative;
|
||||||
animation: rotate 1s linear infinite;
|
animation: rotate 1s linear infinite;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
const isEdit = $derived(task.id !== undefined);
|
const isEdit = $derived(task.id !== undefined);
|
||||||
|
|
||||||
let mcpServers: McpServer[] = $state([]);
|
let mcpServers: McpServer[] = $state([]);
|
||||||
let model: IModel = $state(Model.find(String(task.model)) || Model.default());
|
let model: IModel = $state((task.model && Model.find(task.model)) || Model.default());
|
||||||
|
|
||||||
async function addMcpServer(mcpServer: McpServer) {
|
async function addMcpServer(mcpServer: McpServer) {
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export enum NodeType {
|
|||||||
export enum Interface {
|
export enum Interface {
|
||||||
Voice = 'Voice',
|
Voice = 'Voice',
|
||||||
Chat = 'Chat',
|
Chat = 'Chat',
|
||||||
|
Task = 'Task',
|
||||||
Dashboard = 'Dashboard',
|
Dashboard = 'Dashboard',
|
||||||
Daemon = 'Daemon',
|
Daemon = 'Daemon',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ interface Row {
|
|||||||
app_id: number;
|
app_id: number;
|
||||||
summary: string;
|
summary: string;
|
||||||
config: string;
|
config: string;
|
||||||
|
ephemeral: string;
|
||||||
created: string;
|
created: string;
|
||||||
modified: string;
|
modified: string;
|
||||||
}
|
}
|
||||||
@@ -51,6 +52,7 @@ export default class Session extends Base<Row>('sessions') {
|
|||||||
appId?: number = $state();
|
appId?: number = $state();
|
||||||
summary: string = $state(DEFAULT_SUMMARY);
|
summary: string = $state(DEFAULT_SUMMARY);
|
||||||
config: Partial<Config> = $state({});
|
config: Partial<Config> = $state({});
|
||||||
|
ephemeral: boolean = $state(false);
|
||||||
created?: moment.Moment = $state();
|
created?: moment.Moment = $state();
|
||||||
modified?: moment.Moment = $state();
|
modified?: moment.Moment = $state();
|
||||||
|
|
||||||
@@ -161,11 +163,13 @@ export default class Session extends Base<Row>('sessions') {
|
|||||||
content: Setting.CustomSystemPrompt?.trim() || SYSTEM_PROMPT,
|
content: Setting.CustomSystemPrompt?.trim() || SYSTEM_PROMPT,
|
||||||
});
|
});
|
||||||
|
|
||||||
await Message.create({
|
if (!this.ephemeral) {
|
||||||
sessionId: this.id,
|
await Message.create({
|
||||||
role: 'assistant',
|
sessionId: this.id,
|
||||||
content: WELCOME_PROMPT,
|
role: 'assistant',
|
||||||
});
|
content: WELCOME_PROMPT,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static async fromSql(row: Row): Promise<Session> {
|
protected static async fromSql(row: Row): Promise<Session> {
|
||||||
@@ -174,6 +178,7 @@ export default class Session extends Base<Row>('sessions') {
|
|||||||
appId: row.app_id,
|
appId: row.app_id,
|
||||||
summary: row.summary,
|
summary: row.summary,
|
||||||
config: JSON.parse(row.config),
|
config: JSON.parse(row.config),
|
||||||
|
ephemeral: row.ephemeral === 'true',
|
||||||
created: moment.utc(row.created),
|
created: moment.utc(row.created),
|
||||||
modified: moment.utc(row.modified),
|
modified: moment.utc(row.modified),
|
||||||
});
|
});
|
||||||
@@ -184,6 +189,7 @@ export default class Session extends Base<Row>('sessions') {
|
|||||||
app_id: Number(this.appId),
|
app_id: Number(this.appId),
|
||||||
summary: this.summary,
|
summary: this.summary,
|
||||||
config: JSON.stringify(this.config),
|
config: JSON.stringify(this.config),
|
||||||
|
ephemeral: this.ephemeral ? 'true' : 'false',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,20 +3,26 @@ import moment from 'moment';
|
|||||||
import { Session, Task } from '$lib/models';
|
import { Session, Task } from '$lib/models';
|
||||||
import Base, { type ToSqlRow } from '$lib/models/base.svelte';
|
import Base, { type ToSqlRow } from '$lib/models/base.svelte';
|
||||||
|
|
||||||
|
export enum State {
|
||||||
|
Pending = 'Pending',
|
||||||
|
Success = 'Success',
|
||||||
|
Failure = 'Failure',
|
||||||
|
}
|
||||||
|
|
||||||
interface Row {
|
interface Row {
|
||||||
id: number;
|
id: number;
|
||||||
task_id: number;
|
task_id: number;
|
||||||
session_id: number;
|
session_id: number;
|
||||||
success: boolean;
|
state: State;
|
||||||
timestamp: string;
|
created: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class TaskRun extends Base<Row>('task_runs') {
|
export default class TaskRun extends Base<Row>('task_runs') {
|
||||||
id?: number = $state();
|
id?: number = $state();
|
||||||
taskId?: number = $state();
|
taskId?: number = $state();
|
||||||
sessionId?: number = $state();
|
sessionId?: number = $state();
|
||||||
success: boolean = $state(false);
|
state: State = $state(State.Pending);
|
||||||
timestamp?: moment.Moment = $state();
|
created?: moment.Moment = $state();
|
||||||
|
|
||||||
get task() {
|
get task() {
|
||||||
return Task.find(Number(this.taskId));
|
return Task.find(Number(this.taskId));
|
||||||
@@ -31,8 +37,8 @@ export default class TaskRun extends Base<Row>('task_runs') {
|
|||||||
id: row.id,
|
id: row.id,
|
||||||
taskId: row.task_id,
|
taskId: row.task_id,
|
||||||
sessionId: row.session_id,
|
sessionId: row.session_id,
|
||||||
success: row.success,
|
state: row.state,
|
||||||
timestamp: moment.utc(row.timestamp),
|
created: moment.utc(row.created),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,8 +46,7 @@ export default class TaskRun extends Base<Row>('task_runs') {
|
|||||||
return {
|
return {
|
||||||
task_id: Number(this.taskId),
|
task_id: Number(this.taskId),
|
||||||
session_id: Number(this.sessionId),
|
session_id: Number(this.sessionId),
|
||||||
success: this.success,
|
state: this.state,
|
||||||
timestamp: this.timestamp?.toString() as string,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ interface Row {
|
|||||||
export default class Task extends Base<Row>('tasks') {
|
export default class Task extends Base<Row>('tasks') {
|
||||||
id?: number = $state();
|
id?: number = $state();
|
||||||
name: string = $state('New Task');
|
name: string = $state('New Task');
|
||||||
engineId?: number = $state();
|
engineId: number = $state(Model.default().engineId);
|
||||||
model?: string = $state();
|
model: string = $state(Model.default().id);
|
||||||
prompt: string = $state('');
|
prompt: string = $state('');
|
||||||
period: string = $state('0 12 * * *');
|
period: string = $state('0 12 * * *');
|
||||||
next_run: Date = $state(new Date('2099-12-31T11:59:59.999Z'));
|
next_run: Date = $state(new Date('2099-12-31T11:59:59.999Z'));
|
||||||
|
|||||||
@@ -1,3 +1,54 @@
|
|||||||
import type { Task } from './models';
|
import { dispatch } from './dispatch';
|
||||||
|
import { Engine, Session, Task, TaskRun } from './models';
|
||||||
|
import { State } from './models/task-run.svelte';
|
||||||
|
|
||||||
export function execute(task: Task) {}
|
const TASK_APP_ID = 2;
|
||||||
|
|
||||||
|
export async function execute(task: Task): Promise<TaskRun | undefined> {
|
||||||
|
if (!task.engineId || !task.model) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const engine = Engine.find(task.engineId);
|
||||||
|
const model = engine.models.find(m => m.id == task.model);
|
||||||
|
|
||||||
|
if (!model) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await Session.create({
|
||||||
|
appId: TASK_APP_ID,
|
||||||
|
config: {
|
||||||
|
engineId: task.engineId,
|
||||||
|
model: task.model,
|
||||||
|
enabledMcpServers: task.mcpServers.map(m => m.name),
|
||||||
|
},
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
task.mcpServers.forEach(async server => {
|
||||||
|
await session.addMcpServer(server);
|
||||||
|
await server.start(session);
|
||||||
|
});
|
||||||
|
|
||||||
|
const run = await TaskRun.create({
|
||||||
|
taskId: task.id,
|
||||||
|
sessionId: session.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(session, model, task.prompt)
|
||||||
|
.then(_ => {
|
||||||
|
run.state = State.Success;
|
||||||
|
run.save();
|
||||||
|
})
|
||||||
|
.catch(_ => {
|
||||||
|
run.state = State.Failure;
|
||||||
|
run.save();
|
||||||
|
});
|
||||||
|
|
||||||
|
return run;
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const sessions: Session[] = $derived(Session.all());
|
const sessions: Session[] = $derived(Session.where({ ephemeral: false }));
|
||||||
const mcpServers: McpServer[] = $derived(McpServer.all());
|
const mcpServers: McpServer[] = $derived(McpServer.all());
|
||||||
const engines: Engine[] = $derived(Engine.all());
|
const engines: Engine[] = $derived(Engine.all());
|
||||||
const hasModels = $derived(engines.flatMap(e => e.models).length > 0);
|
const hasModels = $derived(engines.flatMap(e => e.models).length > 0);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
import Menu from '$components/Menu.svelte';
|
import Menu from '$components/Menu.svelte';
|
||||||
import Titlebar from '$components/Titlebar.svelte';
|
import Titlebar from '$components/Titlebar.svelte';
|
||||||
import Task from '$lib/models/task.svelte';
|
import Task from '$lib/models/task.svelte';
|
||||||
|
import { execute } from '$lib/tasks';
|
||||||
|
|
||||||
const { children } = $props();
|
const { children } = $props();
|
||||||
const tasks: Task[] = $derived(Task.all());
|
const tasks: Task[] = $derived(Task.all());
|
||||||
@@ -42,7 +43,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function run(task: Task) {
|
async function run(task: Task) {
|
||||||
// noop for now
|
const run = await execute(task);
|
||||||
|
|
||||||
|
if (run) {
|
||||||
|
await goto(`/tasks/${task.id}/runs/${run.id}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,23 @@
|
|||||||
import Link from '$components/Link.svelte';
|
import Link from '$components/Link.svelte';
|
||||||
import List from '$components/List.svelte';
|
import List from '$components/List.svelte';
|
||||||
import Message from '$components/Message.svelte';
|
import Message from '$components/Message.svelte';
|
||||||
|
import Spinner from '$components/Spinner.svelte';
|
||||||
import Svg from '$components/Svg.svelte';
|
import Svg from '$components/Svg.svelte';
|
||||||
import { TaskRun } from '$lib/models';
|
import { TaskRun } from '$lib/models';
|
||||||
import Task from '$lib/models/task.svelte';
|
import Task from '$lib/models/task.svelte';
|
||||||
|
import { State } from '$lib/models/task-run.svelte';
|
||||||
|
|
||||||
const task: Task = $derived(Task.find(Number(page.params.task_id)));
|
const task: Task = $derived(Task.find(Number(page.params.task_id)));
|
||||||
const run: TaskRun = $derived(TaskRun.find(Number(page.params.run_id)));
|
const run: TaskRun = $derived(TaskRun.find(Number(page.params.run_id)));
|
||||||
|
|
||||||
|
// svelte-ignore non_reactive_update
|
||||||
|
let content: HTMLDivElement;
|
||||||
|
|
||||||
|
function scrollToBottom(_: HTMLDivElement) {
|
||||||
|
if (content) {
|
||||||
|
content.scroll({ top: 9e15 });
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet RunView(run: TaskRun)}
|
{#snippet RunView(run: TaskRun)}
|
||||||
@@ -19,28 +30,31 @@
|
|||||||
class="text-medium flex flex-row items-center px-7 py-2"
|
class="text-medium flex flex-row items-center px-7 py-2"
|
||||||
activeClass="text-purple border-l border-l-purple"
|
activeClass="text-purple border-l border-l-purple"
|
||||||
>
|
>
|
||||||
{#if run.success}
|
{#if run.state == State.Pending}
|
||||||
|
<Spinner class="h-4 w-4 before:border-[2px] before:border-white/30" />
|
||||||
|
{:else if run.state == State.Success}
|
||||||
<Svg name="Check" class="text-green h-4 w-4" />
|
<Svg name="Check" class="text-green h-4 w-4" />
|
||||||
{:else}
|
{:else}
|
||||||
<Svg name="Warning" class="text-red h-4 w-4" />
|
<Svg name="Warning" class="text-red h-4 w-4" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<p class="ml-4">{run.timestamp?.format('LLLL')}</p>
|
<p class="ml-4">{run.created?.format('LLLL')}</p>
|
||||||
</Link>
|
</Link>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{#key page.params.task_id}
|
{#key page.params.task_id}
|
||||||
<Flex class="h-full w-full flex-col items-start">
|
<Flex class="h-full w-full flex-col items-start">
|
||||||
<Flex class="h-3/5 w-full flex-col items-start overflow-y-scroll p-8">
|
<Flex bind:ref={content} class="h-3/5 w-full flex-col items-start overflow-y-scroll p-8">
|
||||||
{#if run}
|
{#if run}
|
||||||
{#each run.session.messages as message (message.id)}
|
{#each run.session.messages as message (message.id)}
|
||||||
|
<div use:scrollToBottom class="hidden"></div>
|
||||||
<Message {message} />
|
<Message {message} />
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex class="border-t-light h-2/5 w-full flex-col items-start border-t py-4">
|
<Flex class="border-t-light h-2/5 w-full flex-col items-start border-t">
|
||||||
<h3 class="mb-4 ml-8 uppercase">History</h3>
|
<h3 class="bg-medium w-full py-2 pl-8 font-medium uppercase">History</h3>
|
||||||
<List items={task?.runs} itemView={RunView} class="border-t-light border-t" />
|
<List items={task?.runs} itemView={RunView} class="border-t-light border-t" />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
Reference in New Issue
Block a user