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,
|
||||
description: "add_tasks_support",
|
||||
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 model TEXT NOT NULL;
|
||||
|
||||
@@ -257,8 +261,8 @@ CREATE TABLE IF NOT EXISTS task_runs (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
task_id INTEGER NOT NULL,
|
||||
session_id INTEGER NOT NULL,
|
||||
success BOOLEAN NOT NULL,
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
state TEXT NOT NULL DEFAULT "Pending",
|
||||
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY(task_id) REFERENCES tasks(id),
|
||||
FOREIGN KEY(session_id) REFERENCES sessions(id)
|
||||
);
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
}
|
||||
</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}
|
||||
<p class={twMerge('text-medium text-sm', titleClass?.toString())}>
|
||||
{title}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
const { class: cls = '' } = $props();
|
||||
const { class: cls = 'w-[24px] h-[24px]' } = $props();
|
||||
</script>
|
||||
|
||||
<div class={twMerge('spinner', cls?.toString())}></div>
|
||||
|
||||
<style>
|
||||
.spinner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
animation: rotate 1s linear infinite;
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
const isEdit = $derived(task.id !== undefined);
|
||||
|
||||
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) {
|
||||
if (isEdit) {
|
||||
|
||||
@@ -28,6 +28,7 @@ export enum NodeType {
|
||||
export enum Interface {
|
||||
Voice = 'Voice',
|
||||
Chat = 'Chat',
|
||||
Task = 'Task',
|
||||
Dashboard = 'Dashboard',
|
||||
Daemon = 'Daemon',
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ interface Row {
|
||||
app_id: number;
|
||||
summary: string;
|
||||
config: string;
|
||||
ephemeral: string;
|
||||
created: string;
|
||||
modified: string;
|
||||
}
|
||||
@@ -51,6 +52,7 @@ export default class Session extends Base<Row>('sessions') {
|
||||
appId?: number = $state();
|
||||
summary: string = $state(DEFAULT_SUMMARY);
|
||||
config: Partial<Config> = $state({});
|
||||
ephemeral: boolean = $state(false);
|
||||
created?: 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,
|
||||
});
|
||||
|
||||
await Message.create({
|
||||
sessionId: this.id,
|
||||
role: 'assistant',
|
||||
content: WELCOME_PROMPT,
|
||||
});
|
||||
if (!this.ephemeral) {
|
||||
await Message.create({
|
||||
sessionId: this.id,
|
||||
role: 'assistant',
|
||||
content: WELCOME_PROMPT,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected static async fromSql(row: Row): Promise<Session> {
|
||||
@@ -174,6 +178,7 @@ export default class Session extends Base<Row>('sessions') {
|
||||
appId: row.app_id,
|
||||
summary: row.summary,
|
||||
config: JSON.parse(row.config),
|
||||
ephemeral: row.ephemeral === 'true',
|
||||
created: moment.utc(row.created),
|
||||
modified: moment.utc(row.modified),
|
||||
});
|
||||
@@ -184,6 +189,7 @@ export default class Session extends Base<Row>('sessions') {
|
||||
app_id: Number(this.appId),
|
||||
summary: this.summary,
|
||||
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 Base, { type ToSqlRow } from '$lib/models/base.svelte';
|
||||
|
||||
export enum State {
|
||||
Pending = 'Pending',
|
||||
Success = 'Success',
|
||||
Failure = 'Failure',
|
||||
}
|
||||
|
||||
interface Row {
|
||||
id: number;
|
||||
task_id: number;
|
||||
session_id: number;
|
||||
success: boolean;
|
||||
timestamp: string;
|
||||
state: State;
|
||||
created: string;
|
||||
}
|
||||
|
||||
export default class TaskRun extends Base<Row>('task_runs') {
|
||||
id?: number = $state();
|
||||
taskId?: number = $state();
|
||||
sessionId?: number = $state();
|
||||
success: boolean = $state(false);
|
||||
timestamp?: moment.Moment = $state();
|
||||
state: State = $state(State.Pending);
|
||||
created?: moment.Moment = $state();
|
||||
|
||||
get task() {
|
||||
return Task.find(Number(this.taskId));
|
||||
@@ -31,8 +37,8 @@ export default class TaskRun extends Base<Row>('task_runs') {
|
||||
id: row.id,
|
||||
taskId: row.task_id,
|
||||
sessionId: row.session_id,
|
||||
success: row.success,
|
||||
timestamp: moment.utc(row.timestamp),
|
||||
state: row.state,
|
||||
created: moment.utc(row.created),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -40,8 +46,7 @@ export default class TaskRun extends Base<Row>('task_runs') {
|
||||
return {
|
||||
task_id: Number(this.taskId),
|
||||
session_id: Number(this.sessionId),
|
||||
success: this.success,
|
||||
timestamp: this.timestamp?.toString() as string,
|
||||
state: this.state,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ interface Row {
|
||||
export default class Task extends Base<Row>('tasks') {
|
||||
id?: number = $state();
|
||||
name: string = $state('New Task');
|
||||
engineId?: number = $state();
|
||||
model?: string = $state();
|
||||
engineId: number = $state(Model.default().engineId);
|
||||
model: string = $state(Model.default().id);
|
||||
prompt: string = $state('');
|
||||
period: string = $state('0 12 * * *');
|
||||
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 engines: Engine[] = $derived(Engine.all());
|
||||
const hasModels = $derived(engines.flatMap(e => e.models).length > 0);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import Menu from '$components/Menu.svelte';
|
||||
import Titlebar from '$components/Titlebar.svelte';
|
||||
import Task from '$lib/models/task.svelte';
|
||||
import { execute } from '$lib/tasks';
|
||||
|
||||
const { children } = $props();
|
||||
const tasks: Task[] = $derived(Task.all());
|
||||
@@ -42,7 +43,11 @@
|
||||
}
|
||||
|
||||
async function run(task: Task) {
|
||||
// noop for now
|
||||
const run = await execute(task);
|
||||
|
||||
if (run) {
|
||||
await goto(`/tasks/${task.id}/runs/${run.id}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -5,12 +5,23 @@
|
||||
import Link from '$components/Link.svelte';
|
||||
import List from '$components/List.svelte';
|
||||
import Message from '$components/Message.svelte';
|
||||
import Spinner from '$components/Spinner.svelte';
|
||||
import Svg from '$components/Svg.svelte';
|
||||
import { TaskRun } from '$lib/models';
|
||||
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 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>
|
||||
|
||||
{#snippet RunView(run: TaskRun)}
|
||||
@@ -19,28 +30,31 @@
|
||||
class="text-medium flex flex-row items-center px-7 py-2"
|
||||
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" />
|
||||
{:else}
|
||||
<Svg name="Warning" class="text-red h-4 w-4" />
|
||||
{/if}
|
||||
|
||||
<p class="ml-4">{run.timestamp?.format('LLLL')}</p>
|
||||
<p class="ml-4">{run.created?.format('LLLL')}</p>
|
||||
</Link>
|
||||
{/snippet}
|
||||
|
||||
{#key page.params.task_id}
|
||||
<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}
|
||||
{#each run.session.messages as message (message.id)}
|
||||
<div use:scrollToBottom class="hidden"></div>
|
||||
<Message {message} />
|
||||
{/each}
|
||||
{/if}
|
||||
</Flex>
|
||||
|
||||
<Flex class="border-t-light h-2/5 w-full flex-col items-start border-t py-4">
|
||||
<h3 class="mb-4 ml-8 uppercase">History</h3>
|
||||
<Flex class="border-t-light h-2/5 w-full flex-col items-start border-t">
|
||||
<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" />
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
Reference in New Issue
Block a user