Repository navigation
feat: add sqlite task runtime plugin #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
f049da2
docs: add sqlite task runtime design
zccz14 7c256fa
docs: add sqlite task runtime plan
zccz14 fae1300
feat: add sqlite task runtime plugin scaffold
zccz14 446d9f4
fix: align sqlite runtime scaffold with task 1
zccz14 07c6ce2
test: tighten sqlite runtime scaffold boundary checks
zccz14 009c165
feat: add sqlite task repository
zccz14 0886518
fix: map sqlite task done as boolean
zccz14 ac7cb25
feat: add task runtime status tools
zccz14 24495f1
fix: tighten task runtime status guards
zccz14 19632f3
fix: harden task prompt spec fencing
zccz14 8603199
feat: add sqlite task dispatch flow
zccz14 a4de7c5
fix: tighten sqlite task dispatch handoff
zccz14 4221c26
feat: finalize sqlite task runtime plugin
zccz14 4f579ee
fix: preserve terminal task outcomes
zccz14 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| import { | ||
| buildFollowUpTaskPrompt, | ||
| buildInitialTaskPrompt, | ||
| } from "./task-runtime-sqlite/prompt-builder.js"; | ||
| import { | ||
| createSessionRuntime, | ||
| type SessionRuntimeHost, | ||
| } from "./task-runtime-sqlite/session-runtime.js"; | ||
| import { | ||
| createTaskRepository, | ||
| TASK_STATUSES, | ||
| type TaskStatus, | ||
| } from "./task-runtime-sqlite/task-repository.js"; | ||
|
|
||
| export type SqliteTaskRuntimePluginOptions = { | ||
| host?: SessionRuntimeHost; | ||
| projectDir: string; | ||
| }; | ||
|
|
||
| type ToolContext = { | ||
| sessionID: string; | ||
| }; | ||
|
|
||
| const createTaskBoundTools = (options: SqliteTaskRuntimePluginOptions) => { | ||
| const getRepository = () => | ||
| createTaskRepository({ | ||
| now: () => new Date().toISOString(), | ||
| projectDir: options.projectDir, | ||
| }); | ||
| const getSessionRuntime = () => createSessionRuntime(options.host); | ||
|
|
||
| const dispatchTasks = async () => { | ||
| const repository = getRepository(); | ||
| const sessionRuntime = getSessionRuntime(); | ||
| const results: Array<{ | ||
| action: "continued" | "created" | "reclaimed" | "skipped_busy"; | ||
| sessionID: string; | ||
| taskID: string; | ||
| }> = []; | ||
|
|
||
| for (const task of repository.listUnfinishedTasks()) { | ||
| if (task.session_id) { | ||
| const session = await sessionRuntime.getSession(task.session_id); | ||
|
|
||
| if (session?.busy) { | ||
| results.push({ | ||
| action: "skipped_busy", | ||
| sessionID: task.session_id, | ||
| taskID: task.task_id, | ||
| }); | ||
| continue; | ||
| } | ||
|
|
||
| if (session) { | ||
| await sessionRuntime.sendPrompt( | ||
| session.id, | ||
| buildFollowUpTaskPrompt(task), | ||
| ); | ||
| results.push({ | ||
| action: "continued", | ||
| sessionID: session.id, | ||
| taskID: task.task_id, | ||
| }); | ||
| continue; | ||
| } | ||
| } | ||
|
|
||
| const session = await sessionRuntime.createSession(); | ||
| const previousSessionID = task.session_id; | ||
|
|
||
| repository.assignSession(task.task_id, session.id); | ||
|
|
||
| try { | ||
| await sessionRuntime.sendPrompt( | ||
| session.id, | ||
| buildInitialTaskPrompt({ ...task, session_id: session.id }), | ||
| ); | ||
| } catch (error) { | ||
| repository.assignSession(task.task_id, previousSessionID); | ||
| throw error; | ||
| } | ||
|
|
||
| results.push({ | ||
| action: task.session_id ? "reclaimed" : "created", | ||
| sessionID: session.id, | ||
| taskID: task.task_id, | ||
| }); | ||
| } | ||
|
|
||
| return results; | ||
| }; | ||
|
|
||
| return { | ||
| "dispatch-tasks": { | ||
| schema: {}, | ||
| execute: dispatchTasks, | ||
| }, | ||
| "list-processing-tasks": { | ||
| schema: {}, | ||
| execute: () => getRepository().listProcessingTasks(), | ||
| }, | ||
| "get-current-task": { | ||
| schema: {}, | ||
| execute: (_input: Record<string, never>, context: ToolContext) => | ||
| getRepository().getRequiredTaskBySessionID(context.sessionID), | ||
| }, | ||
| "mark-task-status": { | ||
| schema: { status: [...TASK_STATUSES] }, | ||
| execute: (input: { status: TaskStatus }, context: ToolContext) => | ||
| getRepository().markTaskStatus({ | ||
| sessionID: context.sessionID, | ||
| status: input.status, | ||
| }), | ||
| }, | ||
| "setup-worktree-path": { | ||
| schema: { worktreePath: true }, | ||
| execute: (input: { worktreePath: string }, context: ToolContext) => | ||
| getRepository().setupWorktreePath({ | ||
| sessionID: context.sessionID, | ||
| worktreePath: input.worktreePath, | ||
| }), | ||
| }, | ||
| "setup-pull-request-url": { | ||
| schema: { pullRequestURL: true }, | ||
| execute: (input: { pullRequestURL: string }, context: ToolContext) => | ||
| getRepository().setupPullRequestURL({ | ||
| sessionID: context.sessionID, | ||
| pullRequestURL: input.pullRequestURL, | ||
| }), | ||
| }, | ||
| }; | ||
| }; | ||
|
|
||
| export const createSqliteTaskRuntimePlugin = ( | ||
| options: SqliteTaskRuntimePluginOptions, | ||
| ) => ({ | ||
| name: "sqlite-task-runtime", | ||
| tools: createTaskBoundTools(options), | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import { join } from "node:path"; | ||
| import { DatabaseSync } from "node:sqlite"; | ||
|
|
||
| export type TaskRecord = { | ||
| task_id: string; | ||
| task_spec: string; | ||
| session_id: string | null; | ||
| worktree_path: string | null; | ||
| pull_request_url: string | null; | ||
| status: string | null; | ||
| done: boolean; | ||
| updated_at: string | null; | ||
| }; | ||
|
|
||
| export type TaskRuntimeDatabase = { | ||
| projectDir: string; | ||
| path: string; | ||
| open(): DatabaseSync; | ||
| }; | ||
|
|
||
| export const getTaskRuntimeDatabasePath = (projectDir: string) => | ||
| join(projectDir, "aim.sqlite"); | ||
|
|
||
| export const openTaskRuntimeDatabase = (projectDir: string) => | ||
| new DatabaseSync(getTaskRuntimeDatabasePath(projectDir)); | ||
|
|
||
| export const createTaskRuntimeDatabase = ( | ||
| projectDir: string, | ||
| ): TaskRuntimeDatabase => ({ | ||
| projectDir, | ||
| path: getTaskRuntimeDatabasePath(projectDir), | ||
| open: () => openTaskRuntimeDatabase(projectDir), | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import type { TaskRecord } from "./database.js"; | ||
|
|
||
| const buildCodeFence = (value: string) => { | ||
| const backtickRuns = value.match(/`+/g) ?? []; | ||
| const longestRun = backtickRuns.reduce( | ||
| (max, run) => Math.max(max, run.length), | ||
| 0, | ||
| ); | ||
|
|
||
| return "`".repeat(Math.max(3, longestRun + 1)); | ||
| }; | ||
|
|
||
| const formatTaskSnapshot = (task: TaskRecord) => | ||
| [ | ||
| `task_id: ${task.task_id}`, | ||
| "task_spec:", | ||
| `${buildCodeFence(task.task_spec)}text`, | ||
| task.task_spec, | ||
| buildCodeFence(task.task_spec), | ||
| `status: ${task.status ?? "unknown"}`, | ||
| `worktree_path: ${task.worktree_path ?? "not set"}`, | ||
| `pull_request_url: ${task.pull_request_url ?? "not set"}`, | ||
| ].join("\n"); | ||
|
|
||
| export const buildInitialTaskPrompt = (task: TaskRecord) => | ||
| [ | ||
| "你正在处理一条已绑定的 Task。", | ||
| "请根据下面的任务快照开始推进当前任务:", | ||
| formatTaskSnapshot(task), | ||
| ].join("\n\n"); | ||
|
|
||
| export const buildFollowUpTaskPrompt = (task: TaskRecord) => | ||
| [ | ||
| "继续推进当前 Task。", | ||
| "以下是当前任务快照:", | ||
| formatTaskSnapshot(task), | ||
| ].join("\n\n"); | ||
|
|
||
| export type PromptBuilder = { | ||
| buildInitialTaskPrompt(task: TaskRecord): string; | ||
| buildFollowUpTaskPrompt(task: TaskRecord): string; | ||
| }; | ||
|
|
||
| export const createPromptBuilder = (): PromptBuilder => ({ | ||
| buildInitialTaskPrompt, | ||
| buildFollowUpTaskPrompt, | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| export type SessionRecord = { | ||
| id: string; | ||
| busy?: boolean; | ||
| }; | ||
|
|
||
| export type SessionRuntimeHost = { | ||
| getSession?( | ||
| sessionID: string, | ||
| ): Promise<SessionRecord | null> | SessionRecord | null; | ||
| createSession(): Promise<SessionRecord> | SessionRecord; | ||
| sendPrompt(sessionID: string, prompt: string): Promise<void> | void; | ||
| }; | ||
|
|
||
| export type SessionRuntime = { | ||
| getSession(sessionID: string): Promise<SessionRecord | null>; | ||
| createSession(): Promise<SessionRecord>; | ||
| sendPrompt(sessionID: string, prompt: string): Promise<void>; | ||
| }; | ||
|
|
||
| export const createSessionRuntime = ( | ||
| host?: SessionRuntimeHost, | ||
| ): SessionRuntime => ({ | ||
| async getSession(sessionID) { | ||
| return (await host?.getSession?.(sessionID)) ?? null; | ||
| }, | ||
|
|
||
| async createSession() { | ||
| if (!host) { | ||
| throw new Error("Session host does not support createSession()"); | ||
| } | ||
|
|
||
| return host.createSession(); | ||
| }, | ||
|
|
||
| async sendPrompt(sessionID, prompt) { | ||
| if (!host) { | ||
| throw new Error("Session host does not support sendPrompt()"); | ||
| } | ||
|
|
||
| await host.sendPrompt(sessionID, prompt); | ||
| }, | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
openTaskRuntimeDatabase()usesnew DatabaseSync(path)which will createaim.sqlitewhen it doesn’t exist. The design spec in this PR says the plugin should error when the DB is missing or schema is invalid; auto-creating an empty DB can hide misconfiguration and lead to dispatch operating against the wrong file. Consider checking file existence (and optionally opening read-only if supported) and throwing a clear error before creating/using the DB.