Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cf9e798e48 | |||
| c36703b4fa | |||
| cc8431e21f | |||
| cd2535fb59 | |||
| f81c00647f | |||
| 2b75c10b60 | |||
| 175821b782 | |||
| af96af716f | |||
| 11ef239dbe |
@@ -1,3 +1,5 @@
|
||||
export * as Log from "./log"
|
||||
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { createWriteStream } from "fs"
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
CREATE TABLE `session_message` (
|
||||
`id` text PRIMARY KEY,
|
||||
`session_id` text NOT NULL,
|
||||
`type` text NOT NULL,
|
||||
`time_created` integer NOT NULL,
|
||||
`time_updated` integer NOT NULL,
|
||||
`data` text NOT NULL,
|
||||
CONSTRAINT `fk_session_message_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
|
||||
);
|
||||
--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS `session_entry_session_idx`;--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS `session_entry_session_type_idx`;--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS `session_entry_time_created_idx`;--> statement-breakpoint
|
||||
CREATE INDEX `session_message_session_idx` ON `session_message` (`session_id`);--> statement-breakpoint
|
||||
CREATE INDEX `session_message_session_type_idx` ON `session_message` (`session_id`,`type`);--> statement-breakpoint
|
||||
CREATE INDEX `session_message_time_created_idx` ON `session_message` (`time_created`);--> statement-breakpoint
|
||||
DROP TABLE `session_entry`;
|
||||
@@ -0,0 +1,1481 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"id": "61f807f9-6398-4067-be05-804acc2561bc",
|
||||
"prevIds": [
|
||||
"66cbe0d7-def0-451b-b88a-7608513a9b44"
|
||||
],
|
||||
"ddl": [
|
||||
{
|
||||
"name": "account_state",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "account",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "control_account",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "workspace",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "project",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "part",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "permission",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "session_message",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "session",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "todo",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "session_share",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "event_sequence",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "event",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "account_state"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "active_account_id",
|
||||
"entityType": "columns",
|
||||
"table": "account_state"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "active_org_id",
|
||||
"entityType": "columns",
|
||||
"table": "account_state"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "email",
|
||||
"entityType": "columns",
|
||||
"table": "account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "url",
|
||||
"entityType": "columns",
|
||||
"table": "account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "access_token",
|
||||
"entityType": "columns",
|
||||
"table": "account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "refresh_token",
|
||||
"entityType": "columns",
|
||||
"table": "account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "token_expiry",
|
||||
"entityType": "columns",
|
||||
"table": "account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "email",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "url",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "access_token",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "refresh_token",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "token_expiry",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "active",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "type",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "''",
|
||||
"generated": null,
|
||||
"name": "name",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "branch",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "directory",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "extra",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "project_id",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "worktree",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "vcs",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "name",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "icon_url",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "icon_url_override",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "icon_color",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_initialized",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "sandboxes",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "commands",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "message_id",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "project_id",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "type",
|
||||
"entityType": "columns",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "project_id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "workspace_id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "parent_id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "slug",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "directory",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "title",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "version",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "share_url",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_additions",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_deletions",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_files",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_diffs",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "revert",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "permission",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_compacting",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_archived",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "content",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "status",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "priority",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "position",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "secret",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "url",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "aggregate_id",
|
||||
"entityType": "columns",
|
||||
"table": "event_sequence"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "seq",
|
||||
"entityType": "columns",
|
||||
"table": "event_sequence"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "event"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "aggregate_id",
|
||||
"entityType": "columns",
|
||||
"table": "event"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "seq",
|
||||
"entityType": "columns",
|
||||
"table": "event"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "type",
|
||||
"entityType": "columns",
|
||||
"table": "event"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "event"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"active_account_id"
|
||||
],
|
||||
"tableTo": "account",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "SET NULL",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_account_state_active_account_id_account_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "account_state"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"project_id"
|
||||
],
|
||||
"tableTo": "project",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_workspace_project_id_project_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_message_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"message_id"
|
||||
],
|
||||
"tableTo": "message",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_part_message_id_message_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"project_id"
|
||||
],
|
||||
"tableTo": "project",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_permission_project_id_project_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_session_message_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"project_id"
|
||||
],
|
||||
"tableTo": "project",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_session_project_id_project_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_todo_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_session_share_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"aggregate_id"
|
||||
],
|
||||
"tableTo": "event_sequence",
|
||||
"columnsTo": [
|
||||
"aggregate_id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "event"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"email",
|
||||
"url"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "control_account_pk",
|
||||
"entityType": "pks",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"session_id",
|
||||
"position"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "todo_pk",
|
||||
"entityType": "pks",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "account_state_pk",
|
||||
"table": "account_state",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "account_pk",
|
||||
"table": "account",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "workspace_pk",
|
||||
"table": "workspace",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "project_pk",
|
||||
"table": "project",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "message_pk",
|
||||
"table": "message",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "part_pk",
|
||||
"table": "part",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"project_id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "permission_pk",
|
||||
"table": "permission",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "session_message_pk",
|
||||
"table": "session_message",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "session_pk",
|
||||
"table": "session",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "session_share_pk",
|
||||
"table": "session_share",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"aggregate_id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "event_sequence_pk",
|
||||
"table": "event_sequence",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "event_pk",
|
||||
"table": "event",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "time_created",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "message_session_time_created_id_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "message_id",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "part_message_id_id_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "part_session_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_message_session_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "type",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_message_session_type_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "time_created",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_message_time_created_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "project_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_project_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "workspace_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_workspace_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "parent_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_parent_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "todo_session_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "todo"
|
||||
}
|
||||
],
|
||||
"renames": []
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"id": "aaa2ebeb-caa4-478d-8365-4fc595d16856",
|
||||
"prevIds": ["66cbe0d7-def0-451b-b88a-7608513a9b44"],
|
||||
"prevIds": [
|
||||
"61f807f9-6398-4067-be05-804acc2561bc"
|
||||
],
|
||||
"ddl": [
|
||||
{
|
||||
"name": "account_state",
|
||||
@@ -37,7 +39,7 @@
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "session_entry",
|
||||
"name": "session_message",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
@@ -598,7 +600,7 @@
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "session_entry"
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
@@ -608,7 +610,7 @@
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "session_entry"
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
@@ -618,7 +620,7 @@
|
||||
"generated": null,
|
||||
"name": "type",
|
||||
"entityType": "columns",
|
||||
"table": "session_entry"
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
@@ -628,7 +630,7 @@
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "session_entry"
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
@@ -638,7 +640,7 @@
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "session_entry"
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
@@ -648,7 +650,7 @@
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "session_entry"
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
@@ -1051,9 +1053,13 @@
|
||||
"table": "event"
|
||||
},
|
||||
{
|
||||
"columns": ["active_account_id"],
|
||||
"columns": [
|
||||
"active_account_id"
|
||||
],
|
||||
"tableTo": "account",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "SET NULL",
|
||||
"nameExplicit": false,
|
||||
@@ -1062,9 +1068,13 @@
|
||||
"table": "account_state"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"columns": [
|
||||
"project_id"
|
||||
],
|
||||
"tableTo": "project",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1073,9 +1083,13 @@
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1084,9 +1098,13 @@
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"columns": ["message_id"],
|
||||
"columns": [
|
||||
"message_id"
|
||||
],
|
||||
"tableTo": "message",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1095,9 +1113,13 @@
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"columns": [
|
||||
"project_id"
|
||||
],
|
||||
"tableTo": "project",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1106,20 +1128,28 @@
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_session_entry_session_id_session_id_fk",
|
||||
"name": "fk_session_message_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "session_entry"
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"columns": [
|
||||
"project_id"
|
||||
],
|
||||
"tableTo": "project",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1128,9 +1158,13 @@
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1139,9 +1173,13 @@
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1150,9 +1188,13 @@
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"columns": ["aggregate_id"],
|
||||
"columns": [
|
||||
"aggregate_id"
|
||||
],
|
||||
"tableTo": "event_sequence",
|
||||
"columnsTo": ["aggregate_id"],
|
||||
"columnsTo": [
|
||||
"aggregate_id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1161,98 +1203,128 @@
|
||||
"table": "event"
|
||||
},
|
||||
{
|
||||
"columns": ["email", "url"],
|
||||
"columns": [
|
||||
"email",
|
||||
"url"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "control_account_pk",
|
||||
"entityType": "pks",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id", "position"],
|
||||
"columns": [
|
||||
"session_id",
|
||||
"position"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "todo_pk",
|
||||
"entityType": "pks",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "account_state_pk",
|
||||
"table": "account_state",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "account_pk",
|
||||
"table": "account",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "workspace_pk",
|
||||
"table": "workspace",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "project_pk",
|
||||
"table": "project",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "message_pk",
|
||||
"table": "message",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "part_pk",
|
||||
"table": "part",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"columns": [
|
||||
"project_id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "permission_pk",
|
||||
"table": "permission",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "session_entry_pk",
|
||||
"table": "session_entry",
|
||||
"name": "session_message_pk",
|
||||
"table": "session_message",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "session_pk",
|
||||
"table": "session",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["session_id"],
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "session_share_pk",
|
||||
"table": "session_share",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["aggregate_id"],
|
||||
"columns": [
|
||||
"aggregate_id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "event_sequence_pk",
|
||||
"table": "event_sequence",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": ["id"],
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "event_pk",
|
||||
"table": "event",
|
||||
@@ -1322,9 +1394,9 @@
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_entry_session_idx",
|
||||
"name": "session_message_session_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session_entry"
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
@@ -1340,9 +1412,9 @@
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_entry_session_type_idx",
|
||||
"name": "session_message_session_type_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session_entry"
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
@@ -1354,9 +1426,9 @@
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_entry_time_created_idx",
|
||||
"name": "session_message_time_created_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session_entry"
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `session` ADD `agent` text;--> statement-breakpoint
|
||||
ALTER TABLE `session` ADD `model` text;
|
||||
@@ -0,0 +1,1511 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"id": "2ec89846-dcf1-4977-ab5e-244ddc9e3d67",
|
||||
"prevIds": [
|
||||
"aaa2ebeb-caa4-478d-8365-4fc595d16856"
|
||||
],
|
||||
"ddl": [
|
||||
{
|
||||
"name": "account_state",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "account",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "control_account",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "workspace",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "project",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "part",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "permission",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "session_message",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "session",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "todo",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "session_share",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "event_sequence",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"name": "event",
|
||||
"entityType": "tables"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "account_state"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "active_account_id",
|
||||
"entityType": "columns",
|
||||
"table": "account_state"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "active_org_id",
|
||||
"entityType": "columns",
|
||||
"table": "account_state"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "email",
|
||||
"entityType": "columns",
|
||||
"table": "account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "url",
|
||||
"entityType": "columns",
|
||||
"table": "account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "access_token",
|
||||
"entityType": "columns",
|
||||
"table": "account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "refresh_token",
|
||||
"entityType": "columns",
|
||||
"table": "account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "token_expiry",
|
||||
"entityType": "columns",
|
||||
"table": "account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "email",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "url",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "access_token",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "refresh_token",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "token_expiry",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "active",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "type",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "''",
|
||||
"generated": null,
|
||||
"name": "name",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "branch",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "directory",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "extra",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "project_id",
|
||||
"entityType": "columns",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "worktree",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "vcs",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "name",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "icon_url",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "icon_url_override",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "icon_color",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_initialized",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "sandboxes",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "commands",
|
||||
"entityType": "columns",
|
||||
"table": "project"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "message_id",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "project_id",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "type",
|
||||
"entityType": "columns",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "project_id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "workspace_id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "parent_id",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "slug",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "directory",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "path",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "title",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "version",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "share_url",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_additions",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_deletions",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_files",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "summary_diffs",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "revert",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "permission",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "agent",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "model",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_compacting",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_archived",
|
||||
"entityType": "columns",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "content",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "status",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "priority",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "position",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "session_id",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "secret",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "url",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_created",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "time_updated",
|
||||
"entityType": "columns",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "aggregate_id",
|
||||
"entityType": "columns",
|
||||
"table": "event_sequence"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "seq",
|
||||
"entityType": "columns",
|
||||
"table": "event_sequence"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "id",
|
||||
"entityType": "columns",
|
||||
"table": "event"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "aggregate_id",
|
||||
"entityType": "columns",
|
||||
"table": "event"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "seq",
|
||||
"entityType": "columns",
|
||||
"table": "event"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "type",
|
||||
"entityType": "columns",
|
||||
"table": "event"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": null,
|
||||
"generated": null,
|
||||
"name": "data",
|
||||
"entityType": "columns",
|
||||
"table": "event"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"active_account_id"
|
||||
],
|
||||
"tableTo": "account",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "SET NULL",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_account_state_active_account_id_account_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "account_state"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"project_id"
|
||||
],
|
||||
"tableTo": "project",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_workspace_project_id_project_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_message_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"message_id"
|
||||
],
|
||||
"tableTo": "message",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_part_message_id_message_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"project_id"
|
||||
],
|
||||
"tableTo": "project",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_permission_project_id_project_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_session_message_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"project_id"
|
||||
],
|
||||
"tableTo": "project",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_session_project_id_project_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_todo_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"tableTo": "session",
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_session_share_session_id_session_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "session_share"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"aggregate_id"
|
||||
],
|
||||
"tableTo": "event_sequence",
|
||||
"columnsTo": [
|
||||
"aggregate_id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
"name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk",
|
||||
"entityType": "fks",
|
||||
"table": "event"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"email",
|
||||
"url"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "control_account_pk",
|
||||
"entityType": "pks",
|
||||
"table": "control_account"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"session_id",
|
||||
"position"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "todo_pk",
|
||||
"entityType": "pks",
|
||||
"table": "todo"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "account_state_pk",
|
||||
"table": "account_state",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "account_pk",
|
||||
"table": "account",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "workspace_pk",
|
||||
"table": "workspace",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "project_pk",
|
||||
"table": "project",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "message_pk",
|
||||
"table": "message",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "part_pk",
|
||||
"table": "part",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"project_id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "permission_pk",
|
||||
"table": "permission",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "session_message_pk",
|
||||
"table": "session_message",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "session_pk",
|
||||
"table": "session",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"session_id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "session_share_pk",
|
||||
"table": "session_share",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"aggregate_id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "event_sequence_pk",
|
||||
"table": "event_sequence",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"nameExplicit": false,
|
||||
"name": "event_pk",
|
||||
"table": "event",
|
||||
"entityType": "pks"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "time_created",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "message_session_time_created_id_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "message"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "message_id",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "part_message_id_id_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "part_session_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "part"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_message_session_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
},
|
||||
{
|
||||
"value": "type",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_message_session_type_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "time_created",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_message_time_created_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session_message"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "project_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_project_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "workspace_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_workspace_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "parent_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "session_parent_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "session"
|
||||
},
|
||||
{
|
||||
"columns": [
|
||||
{
|
||||
"value": "session_id",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"where": null,
|
||||
"origin": "manual",
|
||||
"name": "todo_session_idx",
|
||||
"entityType": "indexes",
|
||||
"table": "todo"
|
||||
}
|
||||
],
|
||||
"renames": []
|
||||
}
|
||||
@@ -24,6 +24,7 @@ export function payloads() {
|
||||
.map(([type, def]) => {
|
||||
return z
|
||||
.object({
|
||||
id: z.string(),
|
||||
type: z.literal(type),
|
||||
properties: zodObject(def.properties),
|
||||
})
|
||||
@@ -39,6 +40,7 @@ export function effectPayloads() {
|
||||
.entries()
|
||||
.map(([type, def]) =>
|
||||
Schema.Struct({
|
||||
id: Schema.String,
|
||||
type: Schema.Literal(type),
|
||||
properties: def.properties,
|
||||
}).annotate({ identifier: `Event.${type}` }),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { EventEmitter } from "events"
|
||||
import { Identifier } from "@/id/id"
|
||||
|
||||
export type GlobalEvent = {
|
||||
directory?: string
|
||||
@@ -7,6 +8,15 @@ export type GlobalEvent = {
|
||||
payload: any
|
||||
}
|
||||
|
||||
export const GlobalBus = new EventEmitter<{
|
||||
class GlobalBusEmitter extends EventEmitter<{
|
||||
event: [GlobalEvent]
|
||||
}>()
|
||||
}> {
|
||||
override emit(eventName: "event", event: GlobalEvent): boolean {
|
||||
if (event.payload && typeof event.payload === "object" && !("id" in event.payload)) {
|
||||
event.payload.id = event.payload.syncEvent?.id ?? Identifier.create("evt", "ascending")
|
||||
}
|
||||
return super.emit(eventName, event)
|
||||
}
|
||||
}
|
||||
|
||||
export const GlobalBus = new GlobalBusEmitter()
|
||||
|
||||
@@ -5,6 +5,7 @@ import { BusEvent } from "./bus-event"
|
||||
import { GlobalBus } from "./global"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { Identifier } from "@/id/id"
|
||||
|
||||
const log = Log.create({ service: "bus" })
|
||||
|
||||
@@ -18,6 +19,7 @@ export const InstanceDisposed = BusEvent.define(
|
||||
)
|
||||
|
||||
type Payload<D extends BusEvent.Definition = BusEvent.Definition> = {
|
||||
id: string
|
||||
type: D["type"]
|
||||
properties: BusProperties<D>
|
||||
}
|
||||
@@ -28,7 +30,11 @@ type State = {
|
||||
}
|
||||
|
||||
export interface Interface {
|
||||
readonly publish: <D extends BusEvent.Definition>(def: D, properties: BusProperties<D>) => Effect.Effect<void>
|
||||
readonly publish: <D extends BusEvent.Definition>(
|
||||
def: D,
|
||||
properties: BusProperties<D>,
|
||||
options?: { id?: string },
|
||||
) => Effect.Effect<void>
|
||||
readonly subscribe: <D extends BusEvent.Definition>(def: D) => Stream.Stream<Payload<D>>
|
||||
readonly subscribeAll: () => Stream.Stream<Payload>
|
||||
readonly subscribeCallback: <D extends BusEvent.Definition>(
|
||||
@@ -53,6 +59,7 @@ export const layer = Layer.effect(
|
||||
// Publish InstanceDisposed before shutting down so subscribers see it
|
||||
yield* PubSub.publish(wildcard, {
|
||||
type: InstanceDisposed.type,
|
||||
id: createID(),
|
||||
properties: { directory: ctx.directory },
|
||||
})
|
||||
yield* PubSub.shutdown(wildcard)
|
||||
@@ -77,10 +84,10 @@ export const layer = Layer.effect(
|
||||
})
|
||||
}
|
||||
|
||||
function publish<D extends BusEvent.Definition>(def: D, properties: BusProperties<D>) {
|
||||
function publish<D extends BusEvent.Definition>(def: D, properties: BusProperties<D>, options?: { id?: string }) {
|
||||
return Effect.gen(function* () {
|
||||
const s = yield* InstanceState.get(state)
|
||||
const payload: Payload = { type: def.type, properties }
|
||||
const payload: Payload = { id: options?.id ?? createID(), type: def.type, properties }
|
||||
log.info("publishing", { type: def.type })
|
||||
|
||||
const ps = s.typed.get(def.type)
|
||||
@@ -173,8 +180,16 @@ const { runPromise, runSync } = makeRuntime(Service, layer)
|
||||
|
||||
// runSync is safe here because the subscribe chain (InstanceState.get, PubSub.subscribe,
|
||||
// Scope.make, Effect.forkScoped) is entirely synchronous. If any step becomes async, this will throw.
|
||||
export async function publish<D extends BusEvent.Definition>(def: D, properties: BusProperties<D>) {
|
||||
return runPromise((svc) => svc.publish(def, properties))
|
||||
export function createID() {
|
||||
return Identifier.create("evt", "ascending")
|
||||
}
|
||||
|
||||
export async function publish<D extends BusEvent.Definition>(
|
||||
def: D,
|
||||
properties: BusProperties<D>,
|
||||
options?: { id?: string },
|
||||
) {
|
||||
return runPromise((svc) => svc.publish(def, properties, options))
|
||||
}
|
||||
|
||||
export function subscribe<D extends BusEvent.Definition>(def: D, callback: (event: Payload<D>) => unknown) {
|
||||
|
||||
@@ -28,6 +28,7 @@ import { useEvent } from "@tui/context/event"
|
||||
import { SDKProvider, useSDK } from "@tui/context/sdk"
|
||||
import { StartupLoading } from "@tui/component/startup-loading"
|
||||
import { SyncProvider, useSync } from "@tui/context/sync"
|
||||
import { SyncProviderV2 } from "@tui/context/sync-v2"
|
||||
import { LocalProvider, useLocal } from "@tui/context/local"
|
||||
import { DialogModel } from "@tui/component/dialog-model"
|
||||
import { useConnected } from "@tui/component/use-connected"
|
||||
@@ -166,27 +167,29 @@ export function tui(input: {
|
||||
>
|
||||
<ProjectProvider>
|
||||
<SyncProvider>
|
||||
<ThemeProvider mode={mode}>
|
||||
<LocalProvider>
|
||||
<KeybindProvider>
|
||||
<PromptStashProvider>
|
||||
<DialogProvider>
|
||||
<CommandProvider>
|
||||
<FrecencyProvider>
|
||||
<PromptHistoryProvider>
|
||||
<PromptRefProvider>
|
||||
<EditorContextProvider>
|
||||
<App onSnapshot={input.onSnapshot} />
|
||||
</EditorContextProvider>
|
||||
</PromptRefProvider>
|
||||
</PromptHistoryProvider>
|
||||
</FrecencyProvider>
|
||||
</CommandProvider>
|
||||
</DialogProvider>
|
||||
</PromptStashProvider>
|
||||
</KeybindProvider>
|
||||
</LocalProvider>
|
||||
</ThemeProvider>
|
||||
<SyncProviderV2>
|
||||
<ThemeProvider mode={mode}>
|
||||
<LocalProvider>
|
||||
<KeybindProvider>
|
||||
<PromptStashProvider>
|
||||
<DialogProvider>
|
||||
<CommandProvider>
|
||||
<FrecencyProvider>
|
||||
<PromptHistoryProvider>
|
||||
<PromptRefProvider>
|
||||
<EditorContextProvider>
|
||||
<App onSnapshot={input.onSnapshot} />
|
||||
</EditorContextProvider>
|
||||
</PromptRefProvider>
|
||||
</PromptHistoryProvider>
|
||||
</FrecencyProvider>
|
||||
</CommandProvider>
|
||||
</DialogProvider>
|
||||
</PromptStashProvider>
|
||||
</KeybindProvider>
|
||||
</LocalProvider>
|
||||
</ThemeProvider>
|
||||
</SyncProviderV2>
|
||||
</SyncProvider>
|
||||
</ProjectProvider>
|
||||
</SDKProvider>
|
||||
|
||||
@@ -755,9 +755,18 @@ export function Prompt(props: PromptProps) {
|
||||
return false
|
||||
}
|
||||
|
||||
const variant = local.model.variant.current()
|
||||
let sessionID = props.sessionID
|
||||
if (sessionID == null) {
|
||||
const res = await sdk.client.session.create({ workspace: props.workspaceID })
|
||||
const res = await sdk.client.session.create({
|
||||
workspace: props.workspaceID,
|
||||
agent: agent.name,
|
||||
model: {
|
||||
providerID: selectedModel.providerID,
|
||||
id: selectedModel.modelID,
|
||||
variant,
|
||||
},
|
||||
})
|
||||
|
||||
if (res.error) {
|
||||
console.log("Creating a session failed:", res.error)
|
||||
@@ -797,7 +806,6 @@ export function Prompt(props: PromptProps) {
|
||||
|
||||
// Capture mode before it gets reset
|
||||
const currentMode = store.mode
|
||||
const variant = local.model.variant.current()
|
||||
const editorSelection = editorContext()
|
||||
const currentEditorSelectionKey = editorSelectionKey(editorSelection)
|
||||
const editorParts =
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
import { useEvent } from "@tui/context/event"
|
||||
import type {
|
||||
SessionMessage,
|
||||
SessionMessageAssistant,
|
||||
SessionMessageAssistantReasoning,
|
||||
SessionMessageAssistantText,
|
||||
SessionMessageAssistantTool,
|
||||
} from "@opencode-ai/sdk/v2"
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { useSDK } from "./sdk"
|
||||
|
||||
function activeAssistant(messages: SessionMessage[]) {
|
||||
const index = messages.findLastIndex((message) => message.type === "assistant" && !message.time.completed)
|
||||
if (index < 0) return
|
||||
const assistant = messages[index]
|
||||
return assistant?.type === "assistant" ? assistant : undefined
|
||||
}
|
||||
|
||||
function activeCompaction(messages: SessionMessage[]) {
|
||||
const index = messages.findLastIndex((message) => message.type === "compaction")
|
||||
if (index < 0) return
|
||||
const compaction = messages[index]
|
||||
return compaction?.type === "compaction" ? compaction : undefined
|
||||
}
|
||||
|
||||
function latestTool(assistant: SessionMessageAssistant | undefined, callID?: string) {
|
||||
return assistant?.content.findLast(
|
||||
(item): item is SessionMessageAssistantTool => item.type === "tool" && (callID === undefined || item.id === callID),
|
||||
)
|
||||
}
|
||||
|
||||
function latestText(assistant: SessionMessageAssistant | undefined) {
|
||||
return assistant?.content.findLast((item): item is SessionMessageAssistantText => item.type === "text")
|
||||
}
|
||||
|
||||
function latestReasoning(assistant: SessionMessageAssistant | undefined, reasoningID: string) {
|
||||
return assistant?.content.findLast(
|
||||
(item): item is SessionMessageAssistantReasoning => item.type === "reasoning" && item.id === reasoningID,
|
||||
)
|
||||
}
|
||||
|
||||
export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext({
|
||||
name: "SyncV2",
|
||||
init: () => {
|
||||
const [store, setStore] = createStore<{
|
||||
messages: {
|
||||
[sessionID: string]: SessionMessage[]
|
||||
}
|
||||
}>({
|
||||
messages: {},
|
||||
})
|
||||
|
||||
const event = useEvent()
|
||||
const sdk = useSDK()
|
||||
|
||||
function update(sessionID: string, fn: (messages: SessionMessage[]) => void) {
|
||||
setStore(
|
||||
"messages",
|
||||
produce((draft) => {
|
||||
fn((draft[sessionID] ??= []))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
event.subscribe((event) => {
|
||||
switch (event.type) {
|
||||
case "session.next.prompted": {
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
draft.push({
|
||||
id: event.id,
|
||||
type: "user",
|
||||
text: event.properties.prompt.text,
|
||||
files: event.properties.prompt.files,
|
||||
agents: event.properties.prompt.agents,
|
||||
time: { created: event.properties.timestamp },
|
||||
})
|
||||
})
|
||||
break
|
||||
}
|
||||
case "session.next.synthetic":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
draft.push({
|
||||
id: event.id,
|
||||
type: "synthetic",
|
||||
sessionID: event.properties.sessionID,
|
||||
text: event.properties.text,
|
||||
time: { created: event.properties.timestamp },
|
||||
})
|
||||
})
|
||||
break
|
||||
case "session.next.step.started":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
const currentAssistant = activeAssistant(draft)
|
||||
if (currentAssistant) currentAssistant.time.completed = event.properties.timestamp
|
||||
draft.push({
|
||||
id: event.id,
|
||||
type: "assistant",
|
||||
agent: event.properties.agent,
|
||||
model: event.properties.model,
|
||||
content: [],
|
||||
snapshot: event.properties.snapshot ? { start: event.properties.snapshot } : undefined,
|
||||
time: { created: event.properties.timestamp },
|
||||
})
|
||||
})
|
||||
break
|
||||
case "session.next.step.ended":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
const currentAssistant = activeAssistant(draft)
|
||||
if (!currentAssistant) return
|
||||
currentAssistant.time.completed = event.properties.timestamp
|
||||
currentAssistant.finish = event.properties.finish
|
||||
currentAssistant.cost = event.properties.cost
|
||||
currentAssistant.tokens = event.properties.tokens
|
||||
if (event.properties.snapshot)
|
||||
currentAssistant.snapshot = { ...currentAssistant.snapshot, end: event.properties.snapshot }
|
||||
})
|
||||
break
|
||||
case "session.next.text.started":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
activeAssistant(draft)?.content.push({ type: "text", text: "" })
|
||||
})
|
||||
break
|
||||
case "session.next.text.delta":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
const match = latestText(activeAssistant(draft))
|
||||
if (match) match.text += event.properties.delta
|
||||
})
|
||||
break
|
||||
case "session.next.text.ended":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
const match = latestText(activeAssistant(draft))
|
||||
if (match) match.text = event.properties.text
|
||||
})
|
||||
break
|
||||
case "session.next.tool.input.started":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
activeAssistant(draft)?.content.push({
|
||||
type: "tool",
|
||||
id: event.properties.callID,
|
||||
name: event.properties.name,
|
||||
time: { created: event.properties.timestamp },
|
||||
state: { status: "pending", input: "" },
|
||||
})
|
||||
})
|
||||
break
|
||||
case "session.next.tool.input.delta":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
const match = latestTool(activeAssistant(draft), event.properties.callID)
|
||||
if (match?.state.status === "pending") match.state.input += event.properties.delta
|
||||
})
|
||||
break
|
||||
case "session.next.tool.input.ended":
|
||||
break
|
||||
case "session.next.tool.called":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
const match = latestTool(activeAssistant(draft), event.properties.callID)
|
||||
if (!match) return
|
||||
match.time.ran = event.properties.timestamp
|
||||
match.provider = event.properties.provider
|
||||
match.state = { status: "running", input: event.properties.input, structured: {}, content: [] }
|
||||
})
|
||||
break
|
||||
case "session.next.tool.progress":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
const match = latestTool(activeAssistant(draft), event.properties.callID)
|
||||
if (match?.state.status !== "running") return
|
||||
match.state.structured = event.properties.structured
|
||||
match.state.content = [...event.properties.content]
|
||||
})
|
||||
break
|
||||
case "session.next.tool.success":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
const match = latestTool(activeAssistant(draft), event.properties.callID)
|
||||
if (match?.state.status !== "running") return
|
||||
match.state = {
|
||||
status: "completed",
|
||||
input: match.state.input,
|
||||
structured: event.properties.structured,
|
||||
content: [...event.properties.content],
|
||||
}
|
||||
match.provider = event.properties.provider
|
||||
match.time.completed = event.properties.timestamp
|
||||
})
|
||||
break
|
||||
case "session.next.tool.error":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
const match = latestTool(activeAssistant(draft), event.properties.callID)
|
||||
if (match?.state.status !== "running") return
|
||||
match.state = {
|
||||
status: "error",
|
||||
error: event.properties.error,
|
||||
input: match.state.input,
|
||||
structured: match.state.structured,
|
||||
content: match.state.content,
|
||||
}
|
||||
match.provider = event.properties.provider
|
||||
match.time.completed = event.properties.timestamp
|
||||
})
|
||||
break
|
||||
case "session.next.reasoning.started":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
activeAssistant(draft)?.content.push({
|
||||
type: "reasoning",
|
||||
id: event.properties.reasoningID,
|
||||
text: "",
|
||||
})
|
||||
})
|
||||
break
|
||||
case "session.next.reasoning.delta":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
const match = latestReasoning(activeAssistant(draft), event.properties.reasoningID)
|
||||
if (match) match.text += event.properties.delta
|
||||
})
|
||||
break
|
||||
case "session.next.reasoning.ended":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
const match = latestReasoning(activeAssistant(draft), event.properties.reasoningID)
|
||||
if (match) match.text = event.properties.text
|
||||
})
|
||||
break
|
||||
case "session.next.retried":
|
||||
break
|
||||
case "session.next.compaction.started":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
draft.push({
|
||||
id: event.id,
|
||||
type: "compaction",
|
||||
reason: event.properties.reason,
|
||||
summary: "",
|
||||
time: { created: event.properties.timestamp },
|
||||
})
|
||||
})
|
||||
break
|
||||
case "session.next.compaction.delta":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
const match = activeCompaction(draft)
|
||||
if (match) match.summary += event.properties.text
|
||||
})
|
||||
break
|
||||
case "session.next.compaction.ended":
|
||||
update(event.properties.sessionID, (draft) => {
|
||||
const match = activeCompaction(draft)
|
||||
if (!match) return
|
||||
match.summary = event.properties.text
|
||||
match.include = event.properties.include
|
||||
})
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
const result = {
|
||||
data: store,
|
||||
session: {
|
||||
message: {
|
||||
async sync(sessionID: string) {
|
||||
const response = await sdk.client.v2.session.messages({ sessionID })
|
||||
setStore("messages", sessionID, reconcile(response.data?.items ?? []))
|
||||
},
|
||||
fromSession(sessionID: string) {
|
||||
const messages = store.messages[sessionID]
|
||||
if (!messages) return []
|
||||
return messages
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,1011 @@
|
||||
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
||||
import { useSyncV2 } from "@tui/context/sync-v2"
|
||||
import { SplitBorder } from "@tui/component/border"
|
||||
import { Spinner } from "@tui/component/spinner"
|
||||
import { useTheme } from "@tui/context/theme"
|
||||
import { useLocal } from "@tui/context/local"
|
||||
import { useKeyboard, useTerminalDimensions, type JSX } from "@opentui/solid"
|
||||
import type { SyntaxStyle } from "@opentui/core"
|
||||
import { Locale } from "@/util/locale"
|
||||
import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
|
||||
import path from "path"
|
||||
import stripAnsi from "strip-ansi"
|
||||
import type {
|
||||
SessionMessage,
|
||||
SessionMessageAgentSwitched,
|
||||
SessionMessageAssistant,
|
||||
SessionMessageAssistantReasoning,
|
||||
SessionMessageAssistantText,
|
||||
SessionMessageAssistantTool,
|
||||
SessionMessageCompaction,
|
||||
SessionMessageModelSwitched,
|
||||
SessionMessageSynthetic,
|
||||
SessionMessageUser,
|
||||
ToolFileContent,
|
||||
ToolTextContent,
|
||||
} from "@opencode-ai/sdk/v2"
|
||||
import { createEffect, createMemo, For, Match, Show, Switch } from "solid-js"
|
||||
|
||||
const id = "internal:session-v2-debug"
|
||||
const route = "session.v2.messages"
|
||||
|
||||
function currentSessionID(api: TuiPluginApi) {
|
||||
const current = api.route.current
|
||||
if (current.name !== "session") return
|
||||
const sessionID = current.params?.sessionID
|
||||
return typeof sessionID === "string" ? sessionID : undefined
|
||||
}
|
||||
|
||||
function View(props: { api: TuiPluginApi; sessionID: string }) {
|
||||
const sync = useSyncV2()
|
||||
const dimensions = useTerminalDimensions()
|
||||
const { theme, syntax, subtleSyntax } = useTheme()
|
||||
const messages = createMemo(() => sync.data.messages[props.sessionID] ?? [])
|
||||
const renderedMessages = createMemo(() => messages().toReversed())
|
||||
const lastAssistant = createMemo(() => renderedMessages().findLast((message) => message.type === "assistant"))
|
||||
|
||||
createEffect(() => {
|
||||
void sync.session.message.sync(props.sessionID)
|
||||
})
|
||||
|
||||
useKeyboard((event) => {
|
||||
if (event.name !== "escape") return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
props.api.route.navigate("session", { sessionID: props.sessionID })
|
||||
})
|
||||
|
||||
return (
|
||||
<box width={dimensions().width} height={dimensions().height} backgroundColor={theme.background}>
|
||||
<box flexDirection="row">
|
||||
<box flexGrow={1} paddingBottom={1} paddingLeft={2} paddingRight={2} gap={1}>
|
||||
<scrollbox
|
||||
viewportOptions={{ paddingRight: 0 }}
|
||||
verticalScrollbarOptions={{ visible: false }}
|
||||
stickyScroll={true}
|
||||
stickyStart="bottom"
|
||||
flexGrow={1}
|
||||
>
|
||||
<box height={1} />
|
||||
<Show when={messages().length === 0}>
|
||||
<MissingData label="Messages" detail="No v2 messages loaded from useSyncV2 yet." />
|
||||
</Show>
|
||||
<For each={renderedMessages()}>
|
||||
{(message, index) => (
|
||||
<Switch>
|
||||
<Match when={message.type === "user"}>
|
||||
<UserMessage message={message as SessionMessageUser} index={index()} />
|
||||
</Match>
|
||||
<Match when={message.type === "assistant"}>
|
||||
<AssistantMessage
|
||||
message={message as SessionMessageAssistant}
|
||||
last={lastAssistant()?.id === message.id}
|
||||
syntax={syntax()}
|
||||
subtleSyntax={subtleSyntax()}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={message.type === "synthetic"}>
|
||||
<SyntheticMessage message={message as SessionMessageSynthetic} index={index()} />
|
||||
</Match>
|
||||
<Match when={message.type === "compaction"}>
|
||||
<CompactionMessage message={message as SessionMessageCompaction} />
|
||||
</Match>
|
||||
<Match when={message.type === "agent-switched"}>
|
||||
<AgentSwitchedMessage message={message as SessionMessageAgentSwitched} />
|
||||
</Match>
|
||||
<Match when={message.type === "model-switched"}>
|
||||
<ModelSwitchedMessage message={message as SessionMessageModelSwitched} />
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<UnknownMessage message={message} />
|
||||
</Match>
|
||||
</Switch>
|
||||
)}
|
||||
</For>
|
||||
</scrollbox>
|
||||
<MissingData
|
||||
label="Session prompt, permission prompt, question prompt, sidebar"
|
||||
detail="The v2 message endpoint only exposes messages, so these session UI regions cannot be rendered here. Press Esc to return to the live session."
|
||||
/>
|
||||
</box>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
function MissingData(props: { label: string; detail: string }) {
|
||||
const { theme } = useTheme()
|
||||
return (
|
||||
<box
|
||||
border={["left"]}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
borderColor={theme.warning}
|
||||
backgroundColor={theme.backgroundPanel}
|
||||
paddingLeft={2}
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
marginTop={1}
|
||||
flexShrink={0}
|
||||
>
|
||||
<text fg={theme.text}>
|
||||
<span style={{ bg: theme.warning, fg: theme.background, bold: true }}> MISSING DATA </span> {props.label}
|
||||
</text>
|
||||
<text fg={theme.textMuted}>{props.detail}</text>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
function UserMessage(props: { message: SessionMessageUser; index: number }) {
|
||||
const { theme } = useTheme()
|
||||
const attachments = createMemo(() => [...(props.message.files ?? []), ...(props.message.agents ?? [])])
|
||||
return (
|
||||
<box
|
||||
id={props.message.id}
|
||||
border={["left"]}
|
||||
borderColor={theme.primary}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
marginTop={props.index === 0 ? 0 : 1}
|
||||
flexShrink={0}
|
||||
>
|
||||
<box paddingTop={1} paddingBottom={1} paddingLeft={2} backgroundColor={theme.backgroundPanel}>
|
||||
<Show
|
||||
when={props.message.text.trim()}
|
||||
fallback={
|
||||
<MissingData label="User message text" detail={`Message ${props.message.id} has no text field content.`} />
|
||||
}
|
||||
>
|
||||
<text fg={theme.text}>{props.message.text}</text>
|
||||
</Show>
|
||||
<Show when={attachments().length}>
|
||||
<box flexDirection="row" paddingTop={1} gap={1} flexWrap="wrap">
|
||||
<For each={props.message.files ?? []}>
|
||||
{(file) => (
|
||||
<text fg={theme.text}>
|
||||
<span style={{ bg: theme.secondary, fg: theme.background }}> {file.mime} </span>
|
||||
<span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> {file.name ?? file.uri} </span>
|
||||
</text>
|
||||
)}
|
||||
</For>
|
||||
<For each={props.message.agents ?? []}>
|
||||
{(agent) => (
|
||||
<text fg={theme.text}>
|
||||
<span style={{ bg: theme.accent, fg: theme.background }}> agent </span>
|
||||
<span style={{ bg: theme.backgroundElement, fg: theme.textMuted }}> {agent.name} </span>
|
||||
</text>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
</Show>
|
||||
<text fg={theme.textMuted}>{Locale.todayTimeOrDateTime(props.message.time.created)}</text>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
function SyntheticMessage(props: { message: SessionMessageSynthetic; index: number }) {
|
||||
const { theme } = useTheme()
|
||||
return (
|
||||
<box
|
||||
id={props.message.id}
|
||||
border={["left"]}
|
||||
borderColor={theme.backgroundElement}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
marginTop={props.index === 0 ? 0 : 1}
|
||||
paddingLeft={2}
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
backgroundColor={theme.backgroundPanel}
|
||||
flexShrink={0}
|
||||
>
|
||||
<text fg={theme.textMuted}>Synthetic</text>
|
||||
<text fg={theme.text}>{props.message.text}</text>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
function CompactionMessage(props: { message: SessionMessageCompaction }) {
|
||||
const { theme } = useTheme()
|
||||
return (
|
||||
<box
|
||||
marginTop={1}
|
||||
border={["top"]}
|
||||
title={props.message.reason === "auto" ? " Auto Compaction " : " Compaction "}
|
||||
titleAlignment="center"
|
||||
borderColor={theme.borderActive}
|
||||
flexShrink={0}
|
||||
>
|
||||
<Show when={props.message.summary}>
|
||||
<text fg={theme.textMuted}>{props.message.summary}</text>
|
||||
</Show>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
function AgentSwitchedMessage(props: { message: SessionMessageAgentSwitched }) {
|
||||
const { theme } = useTheme()
|
||||
const local = useLocal()
|
||||
return (
|
||||
<box paddingLeft={3} marginTop={1} flexShrink={0}>
|
||||
<text>
|
||||
<span style={{ fg: local.agent.color(props.message.agent) }}>▣ </span>
|
||||
<span style={{ fg: theme.textMuted }}>Switched agent to </span>
|
||||
<span style={{ fg: theme.text }}>{Locale.titlecase(props.message.agent)}</span>
|
||||
</text>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
function ModelSwitchedMessage(props: { message: SessionMessageModelSwitched }) {
|
||||
const { theme } = useTheme()
|
||||
const model = createMemo(() => {
|
||||
const variant = props.message.model.variant ? `/${props.message.model.variant}` : ""
|
||||
return `${props.message.model.providerID}/${props.message.model.id}${variant}`
|
||||
})
|
||||
return (
|
||||
<box paddingLeft={3} marginTop={1} flexShrink={0}>
|
||||
<text>
|
||||
<span style={{ fg: theme.secondary }}>◇ </span>
|
||||
<span style={{ fg: theme.textMuted }}>Switched model to </span>
|
||||
<span style={{ fg: theme.text }}>{model()}</span>
|
||||
</text>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
function UnknownMessage(props: { message: SessionMessage }) {
|
||||
return <MissingData label="Unknown message type" detail={JSON.stringify(props.message)} />
|
||||
}
|
||||
|
||||
function AssistantMessage(props: {
|
||||
message: SessionMessageAssistant
|
||||
last: boolean
|
||||
syntax: SyntaxStyle
|
||||
subtleSyntax: SyntaxStyle
|
||||
}) {
|
||||
const { theme } = useTheme()
|
||||
const local = useLocal()
|
||||
const duration = createMemo(() => {
|
||||
if (!props.message.time.completed) return 0
|
||||
return props.message.time.completed - props.message.time.created
|
||||
})
|
||||
const model = createMemo(() => {
|
||||
const variant = props.message.model.variant ? `/${props.message.model.variant}` : ""
|
||||
return `${props.message.model.providerID}/${props.message.model.id}${variant}`
|
||||
})
|
||||
const final = createMemo(() => props.message.finish && !["tool-calls", "unknown"].includes(props.message.finish))
|
||||
return (
|
||||
<>
|
||||
<For each={props.message.content}>
|
||||
{(part) => (
|
||||
<Switch>
|
||||
<Match when={part.type === "text"}>
|
||||
<AssistantText part={part as SessionMessageAssistantText} syntax={props.syntax} />
|
||||
</Match>
|
||||
<Match when={part.type === "reasoning"}>
|
||||
<AssistantReasoning part={part as SessionMessageAssistantReasoning} subtleSyntax={props.subtleSyntax} />
|
||||
</Match>
|
||||
<Match when={part.type === "tool"}>
|
||||
<AssistantTool part={part as SessionMessageAssistantTool} />
|
||||
</Match>
|
||||
</Switch>
|
||||
)}
|
||||
</For>
|
||||
<Show when={props.message.content.length === 0}>
|
||||
<MissingData label="Assistant content" detail={`Assistant message ${props.message.id} has no content items.`} />
|
||||
</Show>
|
||||
<Show when={props.message.error}>
|
||||
<box
|
||||
border={["left"]}
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
marginTop={1}
|
||||
backgroundColor={theme.backgroundPanel}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
borderColor={theme.error}
|
||||
flexShrink={0}
|
||||
>
|
||||
<text fg={theme.textMuted}>{props.message.error}</text>
|
||||
</box>
|
||||
</Show>
|
||||
<Show when={props.last || final() || props.message.error}>
|
||||
<box paddingLeft={3} flexShrink={0}>
|
||||
<text marginTop={1}>
|
||||
<span style={{ fg: local.agent.color(props.message.agent) }}>▣ </span>
|
||||
<span style={{ fg: theme.text }}>{Locale.titlecase(props.message.agent)}</span>
|
||||
<span style={{ fg: theme.textMuted }}> · {model()}</span>
|
||||
<Show when={duration()}>
|
||||
<span style={{ fg: theme.textMuted }}> · {Locale.duration(duration())}</span>
|
||||
</Show>
|
||||
</text>
|
||||
</box>
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function AssistantText(props: { part: SessionMessageAssistantText; syntax: SyntaxStyle }) {
|
||||
const { theme } = useTheme()
|
||||
return (
|
||||
<Show when={props.part.text.trim()}>
|
||||
<box paddingLeft={3} marginTop={1} flexShrink={0}>
|
||||
<code
|
||||
filetype="markdown"
|
||||
drawUnstyledText={false}
|
||||
streaming={true}
|
||||
syntaxStyle={props.syntax}
|
||||
content={props.part.text.trim()}
|
||||
conceal={true}
|
||||
fg={theme.text}
|
||||
/>
|
||||
</box>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
function AssistantReasoning(props: { part: SessionMessageAssistantReasoning; subtleSyntax: SyntaxStyle }) {
|
||||
const { theme } = useTheme()
|
||||
const content = createMemo(() => props.part.text.replace("[REDACTED]", "").trim())
|
||||
return (
|
||||
<Show when={content()}>
|
||||
<box
|
||||
paddingLeft={2}
|
||||
marginTop={1}
|
||||
flexDirection="column"
|
||||
border={["left"]}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
borderColor={theme.backgroundElement}
|
||||
flexShrink={0}
|
||||
>
|
||||
<code
|
||||
filetype="markdown"
|
||||
drawUnstyledText={false}
|
||||
streaming={true}
|
||||
syntaxStyle={props.subtleSyntax}
|
||||
content={"_Thinking:_ " + content()}
|
||||
conceal={true}
|
||||
fg={theme.textMuted}
|
||||
/>
|
||||
</box>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
function AssistantTool(props: { part: SessionMessageAssistantTool }) {
|
||||
const input = createMemo(() => toolInputRecord(props.part.state.input))
|
||||
const toolprops = {
|
||||
get input() {
|
||||
return input()
|
||||
},
|
||||
get metadata() {
|
||||
return props.part.provider?.metadata ?? {}
|
||||
},
|
||||
get output() {
|
||||
return props.part.state.status === "pending" ? undefined : toolOutput(props.part.state.content)
|
||||
},
|
||||
part: props.part,
|
||||
}
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={props.part.name === "bash"}>
|
||||
<Bash {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.name === "glob"}>
|
||||
<Glob {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.name === "read"}>
|
||||
<Read {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.name === "grep"}>
|
||||
<Grep {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.name === "webfetch"}>
|
||||
<WebFetch {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.name === "codesearch"}>
|
||||
<CodeSearch {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.name === "websearch"}>
|
||||
<WebSearch {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.name === "write"}>
|
||||
<Write {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.name === "edit"}>
|
||||
<Edit {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.name === "apply_patch"}>
|
||||
<ApplyPatch {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.name === "todowrite"}>
|
||||
<TodoWrite {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.name === "question"}>
|
||||
<Question {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.name === "skill"}>
|
||||
<Skill {...toolprops} />
|
||||
</Match>
|
||||
<Match when={props.part.name === "task"}>
|
||||
<Task {...toolprops} />
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<GenericTool {...toolprops} />
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
type ToolProps = {
|
||||
input: Record<string, unknown>
|
||||
metadata: Record<string, unknown>
|
||||
output?: string
|
||||
part: SessionMessageAssistantTool
|
||||
}
|
||||
|
||||
function GenericTool(props: ToolProps) {
|
||||
return (
|
||||
<Show
|
||||
when={props.output?.trim()}
|
||||
fallback={
|
||||
<InlineTool icon="⚙" pending="Writing command..." complete={toolComplete(props.part)} part={props.part}>
|
||||
{props.part.name} {input(props.input)}
|
||||
</InlineTool>
|
||||
}
|
||||
>
|
||||
<BlockTool title={`# ${props.part.name} ${input(props.input)}`} part={props.part}>
|
||||
<text fg={useTheme().theme.text}>{props.output}</text>
|
||||
</BlockTool>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
function InlineTool(props: {
|
||||
icon: string
|
||||
complete: unknown
|
||||
pending: string
|
||||
spinner?: boolean
|
||||
children: JSX.Element
|
||||
part: SessionMessageAssistantTool
|
||||
}) {
|
||||
const { theme } = useTheme()
|
||||
const error = createMemo(() => (props.part.state.status === "error" ? props.part.state.error.message : undefined))
|
||||
const denied = createMemo(() => {
|
||||
const message = error()
|
||||
if (!message) return false
|
||||
return (
|
||||
message.includes("QuestionRejectedError") ||
|
||||
message.includes("rejected permission") ||
|
||||
message.includes("user dismissed")
|
||||
)
|
||||
})
|
||||
return (
|
||||
<box marginTop={1} paddingLeft={3} flexShrink={0}>
|
||||
<Switch>
|
||||
<Match when={props.spinner}>
|
||||
<Spinner color={theme.text}>{props.children}</Spinner>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<text paddingLeft={3} fg={props.complete ? theme.textMuted : theme.text}>
|
||||
<Show fallback={<>~ {props.pending}</>} when={props.complete}>
|
||||
{props.icon} {props.children}
|
||||
</Show>
|
||||
</text>
|
||||
</Match>
|
||||
</Switch>
|
||||
<Show when={error() && !denied()}>
|
||||
<text fg={theme.error}>{error()}</text>
|
||||
</Show>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
function BlockTool(props: {
|
||||
title: string
|
||||
children: JSX.Element
|
||||
part: SessionMessageAssistantTool
|
||||
spinner?: boolean
|
||||
}) {
|
||||
const { theme } = useTheme()
|
||||
const error = createMemo(() => (props.part.state.status === "error" ? props.part.state.error.message : undefined))
|
||||
return (
|
||||
<box
|
||||
border={["left"]}
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
paddingLeft={2}
|
||||
marginTop={1}
|
||||
gap={1}
|
||||
backgroundColor={theme.backgroundPanel}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
borderColor={theme.background}
|
||||
flexShrink={0}
|
||||
>
|
||||
<Show
|
||||
when={props.spinner}
|
||||
fallback={
|
||||
<text paddingLeft={3} fg={theme.textMuted}>
|
||||
{props.title}
|
||||
</text>
|
||||
}
|
||||
>
|
||||
<Spinner color={theme.textMuted}>{props.title.replace(/^# /, "")}</Spinner>
|
||||
</Show>
|
||||
{props.children}
|
||||
<Show when={error()}>
|
||||
<text fg={theme.error}>{error()}</text>
|
||||
</Show>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
function Bash(props: ToolProps) {
|
||||
const { theme } = useTheme()
|
||||
const output = createMemo(() => stripAnsi((stringValue(props.metadata.output) ?? props.output ?? "").trim()))
|
||||
const command = createMemo(() => stringValue(props.input.command) ?? pendingInput(props.part))
|
||||
const title = createMemo(() => `# ${stringValue(props.input.description) ?? "Shell"}`)
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={output()}>
|
||||
<BlockTool title={title()} part={props.part} spinner={props.part.state.status === "running"}>
|
||||
<box gap={1}>
|
||||
<text fg={theme.text}>$ {command()}</text>
|
||||
<text fg={theme.text}>{output()}</text>
|
||||
</box>
|
||||
</BlockTool>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<InlineTool icon="$" pending="Writing command..." complete={command()} part={props.part}>
|
||||
{command()}
|
||||
</InlineTool>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
function Glob(props: ToolProps) {
|
||||
return (
|
||||
<InlineTool icon="✱" pending="Finding files..." complete={toolComplete(props.part)} part={props.part}>
|
||||
Glob "{stringValue(props.input.pattern) ?? pendingInput(props.part)}"{" "}
|
||||
<Show when={stringValue(props.input.path)}>in {normalizePath(stringValue(props.input.path))} </Show>
|
||||
<Show when={numberValue(props.metadata.count)}>
|
||||
{(count) => (
|
||||
<>
|
||||
({count()} {count() === 1 ? "match" : "matches"})
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
</InlineTool>
|
||||
)
|
||||
}
|
||||
|
||||
function Read(props: ToolProps) {
|
||||
const { theme } = useTheme()
|
||||
const loaded = createMemo(() =>
|
||||
arrayValue(props.metadata.loaded).filter((item): item is string => typeof item === "string"),
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<InlineTool
|
||||
icon="→"
|
||||
pending="Reading file..."
|
||||
complete={stringValue(props.input.filePath) ?? pendingInput(props.part)}
|
||||
spinner={props.part.state.status === "running"}
|
||||
part={props.part}
|
||||
>
|
||||
Read {normalizePath(stringValue(props.input.filePath) ?? pendingInput(props.part))}{" "}
|
||||
{input(props.input, ["filePath"])}
|
||||
</InlineTool>
|
||||
<For each={loaded()}>
|
||||
{(filepath) => (
|
||||
<box paddingLeft={3} flexShrink={0}>
|
||||
<text paddingLeft={3} fg={theme.textMuted}>
|
||||
↳ Loaded {normalizePath(filepath)}
|
||||
</text>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function Grep(props: ToolProps) {
|
||||
return (
|
||||
<InlineTool icon="✱" pending="Searching content..." complete={toolComplete(props.part)} part={props.part}>
|
||||
Grep "{stringValue(props.input.pattern) ?? pendingInput(props.part)}"{" "}
|
||||
<Show when={stringValue(props.input.path)}>in {normalizePath(stringValue(props.input.path))} </Show>
|
||||
<Show when={numberValue(props.metadata.matches)}>
|
||||
{(matches) => (
|
||||
<>
|
||||
({matches()} {matches() === 1 ? "match" : "matches"})
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
</InlineTool>
|
||||
)
|
||||
}
|
||||
|
||||
function WebFetch(props: ToolProps) {
|
||||
return (
|
||||
<InlineTool icon="%" pending="Fetching from the web..." complete={toolComplete(props.part)} part={props.part}>
|
||||
WebFetch {stringValue(props.input.url) ?? pendingInput(props.part)}
|
||||
</InlineTool>
|
||||
)
|
||||
}
|
||||
|
||||
function CodeSearch(props: ToolProps) {
|
||||
return (
|
||||
<InlineTool icon="◇" pending="Searching code..." complete={toolComplete(props.part)} part={props.part}>
|
||||
Exa Code Search "{stringValue(props.input.query) ?? pendingInput(props.part)}"{" "}
|
||||
<Show when={numberValue(props.metadata.results)}>{(results) => <>({results()} results)</>}</Show>
|
||||
</InlineTool>
|
||||
)
|
||||
}
|
||||
|
||||
function WebSearch(props: ToolProps) {
|
||||
return (
|
||||
<InlineTool icon="◈" pending="Searching web..." complete={toolComplete(props.part)} part={props.part}>
|
||||
Exa Web Search "{stringValue(props.input.query) ?? pendingInput(props.part)}"{" "}
|
||||
<Show when={numberValue(props.metadata.numResults)}>{(results) => <>({results()} results)</>}</Show>
|
||||
</InlineTool>
|
||||
)
|
||||
}
|
||||
|
||||
function Write(props: ToolProps) {
|
||||
const { theme, syntax } = useTheme()
|
||||
const filePath = createMemo(() => stringValue(props.input.filePath) ?? "")
|
||||
const content = createMemo(() => stringValue(props.input.content) ?? "")
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={content() && props.part.state.status === "completed"}>
|
||||
<BlockTool title={"# Wrote " + normalizePath(filePath())} part={props.part}>
|
||||
<line_number fg={theme.textMuted} minWidth={3} paddingRight={1}>
|
||||
<code
|
||||
conceal={false}
|
||||
fg={theme.text}
|
||||
filetype={filetype(filePath())}
|
||||
syntaxStyle={syntax()}
|
||||
content={content()}
|
||||
/>
|
||||
</line_number>
|
||||
<Diagnostics diagnostics={props.metadata.diagnostics} filePath={filePath()} />
|
||||
</BlockTool>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<InlineTool icon="←" pending="Preparing write..." complete={filePath()} part={props.part}>
|
||||
Write {normalizePath(filePath())}
|
||||
</InlineTool>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
function Edit(props: ToolProps) {
|
||||
const { theme, syntax } = useTheme()
|
||||
const dimensions = useTerminalDimensions()
|
||||
const filePath = createMemo(() => stringValue(props.input.filePath) ?? "")
|
||||
const diff = createMemo(() => stringValue(props.metadata.diff))
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={diff()}>
|
||||
{(diff) => (
|
||||
<BlockTool title={"← Edit " + normalizePath(filePath())} part={props.part}>
|
||||
<box paddingLeft={1}>
|
||||
<diff
|
||||
diff={diff()}
|
||||
view={dimensions().width > 120 ? "split" : "unified"}
|
||||
filetype={filetype(filePath())}
|
||||
syntaxStyle={syntax()}
|
||||
showLineNumbers={true}
|
||||
width="100%"
|
||||
wrapMode="word"
|
||||
fg={theme.text}
|
||||
addedBg={theme.diffAddedBg}
|
||||
removedBg={theme.diffRemovedBg}
|
||||
contextBg={theme.diffContextBg}
|
||||
addedSignColor={theme.diffHighlightAdded}
|
||||
removedSignColor={theme.diffHighlightRemoved}
|
||||
lineNumberFg={theme.diffLineNumber}
|
||||
lineNumberBg={theme.diffContextBg}
|
||||
addedLineNumberBg={theme.diffAddedLineNumberBg}
|
||||
removedLineNumberBg={theme.diffRemovedLineNumberBg}
|
||||
/>
|
||||
</box>
|
||||
<Diagnostics diagnostics={props.metadata.diagnostics} filePath={filePath()} />
|
||||
</BlockTool>
|
||||
)}
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<InlineTool icon="←" pending="Preparing edit..." complete={filePath()} part={props.part}>
|
||||
Edit {normalizePath(filePath())} {input({ replaceAll: props.input.replaceAll })}
|
||||
</InlineTool>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
function ApplyPatch(props: ToolProps) {
|
||||
const { theme, syntax } = useTheme()
|
||||
const dimensions = useTerminalDimensions()
|
||||
const files = createMemo(() => arrayValue(props.metadata.files).flatMap((item) => (isRecord(item) ? [item] : [])))
|
||||
const fileTitle = (file: Record<string, unknown>) => {
|
||||
const type = stringValue(file.type)
|
||||
const relativePath = stringValue(file.relativePath) ?? stringValue(file.filePath) ?? "patch"
|
||||
if (type === "delete") return "# Deleted " + relativePath
|
||||
if (type === "add") return "# Created " + relativePath
|
||||
if (type === "move") return "# Moved " + normalizePath(stringValue(file.filePath)) + " → " + relativePath
|
||||
return "← Patched " + relativePath
|
||||
}
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={files().length > 0}>
|
||||
<For each={files()}>
|
||||
{(file) => (
|
||||
<BlockTool title={fileTitle(file)} part={props.part}>
|
||||
<Show
|
||||
when={stringValue(file.patch)}
|
||||
fallback={
|
||||
<text fg={theme.diffRemoved}>
|
||||
-{numberValue(file.deletions) ?? 0} line{numberValue(file.deletions) === 1 ? "" : "s"}
|
||||
</text>
|
||||
}
|
||||
>
|
||||
{(patch) => (
|
||||
<box paddingLeft={1}>
|
||||
<diff
|
||||
diff={patch()}
|
||||
view={dimensions().width > 120 ? "split" : "unified"}
|
||||
filetype={filetype(stringValue(file.filePath) ?? stringValue(file.relativePath))}
|
||||
syntaxStyle={syntax()}
|
||||
showLineNumbers={true}
|
||||
width="100%"
|
||||
wrapMode="word"
|
||||
fg={theme.text}
|
||||
addedBg={theme.diffAddedBg}
|
||||
removedBg={theme.diffRemovedBg}
|
||||
contextBg={theme.diffContextBg}
|
||||
addedSignColor={theme.diffHighlightAdded}
|
||||
removedSignColor={theme.diffHighlightRemoved}
|
||||
lineNumberFg={theme.diffLineNumber}
|
||||
lineNumberBg={theme.diffContextBg}
|
||||
addedLineNumberBg={theme.diffAddedLineNumberBg}
|
||||
removedLineNumberBg={theme.diffRemovedLineNumberBg}
|
||||
/>
|
||||
</box>
|
||||
)}
|
||||
</Show>
|
||||
</BlockTool>
|
||||
)}
|
||||
</For>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<InlineTool icon="%" pending="Preparing patch..." complete={false} part={props.part}>
|
||||
Patch
|
||||
</InlineTool>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
function TodoWrite(props: ToolProps) {
|
||||
const { theme } = useTheme()
|
||||
const todos = createMemo(() => arrayValue(props.input.todos).flatMap((item) => (isRecord(item) ? [item] : [])))
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={todos().length > 0 && props.part.state.status === "completed"}>
|
||||
<BlockTool title="# Todos" part={props.part}>
|
||||
<box>
|
||||
<For each={todos()}>
|
||||
{(todo) => (
|
||||
<text fg={theme.text}>
|
||||
{todoIcon(stringValue(todo.status))} {stringValue(todo.content)}
|
||||
</text>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
</BlockTool>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<InlineTool icon="⚙" pending="Updating todos..." complete={false} part={props.part}>
|
||||
Updating todos...
|
||||
</InlineTool>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
function Question(props: ToolProps) {
|
||||
const { theme } = useTheme()
|
||||
const questions = createMemo(() =>
|
||||
arrayValue(props.input.questions).flatMap((item) => (isRecord(item) ? [item] : [])),
|
||||
)
|
||||
const answers = createMemo(() => arrayValue(props.metadata.answers))
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={answers().length > 0}>
|
||||
<BlockTool title="# Questions" part={props.part}>
|
||||
<box gap={1}>
|
||||
<For each={questions()}>
|
||||
{(question, index) => (
|
||||
<box>
|
||||
<text fg={theme.textMuted}>{stringValue(question.question)}</text>
|
||||
<text fg={theme.text}>{formatAnswer(answers()[index()])}</text>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
</BlockTool>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<InlineTool icon="→" pending="Asking questions..." complete={questions().length} part={props.part}>
|
||||
Asked {questions().length} question{questions().length === 1 ? "" : "s"}
|
||||
</InlineTool>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
function Skill(props: ToolProps) {
|
||||
return (
|
||||
<InlineTool icon="→" pending="Loading skill..." complete={toolComplete(props.part)} part={props.part}>
|
||||
Skill "{stringValue(props.input.name) ?? pendingInput(props.part)}"
|
||||
</InlineTool>
|
||||
)
|
||||
}
|
||||
|
||||
function Task(props: ToolProps) {
|
||||
const content = createMemo(() => {
|
||||
const description = stringValue(props.input.description)
|
||||
if (!description) return pendingInput(props.part)
|
||||
return `${Locale.titlecase(stringValue(props.input.subagent_type) ?? "General")} Task — ${description}`
|
||||
})
|
||||
return (
|
||||
<InlineTool
|
||||
icon="│"
|
||||
spinner={props.part.state.status === "running"}
|
||||
complete={toolComplete(props.part)}
|
||||
pending="Delegating..."
|
||||
part={props.part}
|
||||
>
|
||||
{content()}
|
||||
</InlineTool>
|
||||
)
|
||||
}
|
||||
|
||||
function Diagnostics(props: { diagnostics: unknown; filePath: string }) {
|
||||
const { theme } = useTheme()
|
||||
const errors = createMemo(() => {
|
||||
if (!isRecord(props.diagnostics)) return []
|
||||
const value = props.diagnostics[normalizePath(props.filePath)] ?? props.diagnostics[props.filePath]
|
||||
return arrayValue(value)
|
||||
.flatMap((item) => (isRecord(item) ? [item] : []))
|
||||
.filter((diagnostic) => diagnostic.severity === 1)
|
||||
.slice(0, 3)
|
||||
})
|
||||
return (
|
||||
<Show when={errors().length}>
|
||||
<box>
|
||||
<For each={errors()}>
|
||||
{(diagnostic) => <text fg={theme.error}>Error {stringValue(diagnostic.message)}</text>}
|
||||
</For>
|
||||
</box>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
function toolOutput(content?: Array<ToolTextContent | ToolFileContent>) {
|
||||
return (content ?? [])
|
||||
.map((item) => {
|
||||
if (item.type === "text") return item.text.trim()
|
||||
return `[file ${item.name ?? item.uri}]`
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
function toolInputRecord(input: string | Record<string, unknown>) {
|
||||
if (typeof input === "string") return {}
|
||||
return input
|
||||
}
|
||||
|
||||
function pendingInput(part: SessionMessageAssistantTool) {
|
||||
if (part.state.status !== "pending") return ""
|
||||
return part.state.input.trim()
|
||||
}
|
||||
|
||||
function toolComplete(part: SessionMessageAssistantTool) {
|
||||
if (part.state.status === "pending") return pendingInput(part)
|
||||
return part.state.status === "completed" || part.state.status === "error" || part.state.status === "running"
|
||||
}
|
||||
|
||||
function stringValue(value: unknown) {
|
||||
return typeof value === "string" ? value : undefined
|
||||
}
|
||||
|
||||
function numberValue(value: unknown) {
|
||||
return typeof value === "number" ? value : undefined
|
||||
}
|
||||
|
||||
function arrayValue(value: unknown): unknown[] {
|
||||
return Array.isArray(value) ? value : []
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === "object" && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function input(input: Record<string, unknown>, omit?: string[]) {
|
||||
const primitives = Object.entries(input).filter(([key, value]) => {
|
||||
if (omit?.includes(key)) return false
|
||||
return typeof value === "string" || typeof value === "number" || typeof value === "boolean"
|
||||
})
|
||||
if (primitives.length === 0) return ""
|
||||
return `[${primitives.map(([key, value]) => `${key}=${value}`).join(", ")}]`
|
||||
}
|
||||
|
||||
function normalizePath(input?: string) {
|
||||
if (!input) return ""
|
||||
const absolute = path.isAbsolute(input) ? input : path.resolve(process.cwd(), input)
|
||||
const relative = path.relative(process.cwd(), absolute)
|
||||
if (!relative) return "."
|
||||
if (!relative.startsWith("..")) return relative
|
||||
return absolute
|
||||
}
|
||||
|
||||
function filetype(input?: string) {
|
||||
if (!input) return "none"
|
||||
const language = LANGUAGE_EXTENSIONS[path.extname(input)]
|
||||
if (["typescriptreact", "javascriptreact", "javascript"].includes(language)) return "typescript"
|
||||
return language
|
||||
}
|
||||
|
||||
function todoIcon(status?: string) {
|
||||
if (status === "completed") return "✓"
|
||||
if (status === "in_progress") return "~"
|
||||
if (status === "cancelled") return "✕"
|
||||
return "☐"
|
||||
}
|
||||
|
||||
function formatAnswer(answer: unknown) {
|
||||
if (!Array.isArray(answer)) return "(no answer)"
|
||||
if (answer.length === 0) return "(no answer)"
|
||||
return answer.filter((item): item is string => typeof item === "string").join(", ")
|
||||
}
|
||||
|
||||
const tui: TuiPlugin = async (api) => {
|
||||
api.route.register([
|
||||
{
|
||||
name: route,
|
||||
render(input) {
|
||||
const sessionID = input.params?.sessionID
|
||||
if (typeof sessionID !== "string") {
|
||||
return <text fg={api.theme.current.error}>Missing sessionID</text>
|
||||
}
|
||||
return <View api={api} sessionID={sessionID} />
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
api.command.register(() => [
|
||||
{
|
||||
title: "View v2 session messages",
|
||||
value: route,
|
||||
category: "Debug",
|
||||
suggested: api.route.current.name === "session",
|
||||
enabled: api.route.current.name === "session",
|
||||
onSelect() {
|
||||
const sessionID = currentSessionID(api)
|
||||
if (!sessionID) return
|
||||
api.route.navigate(route, { sessionID })
|
||||
api.ui.dialog.clear()
|
||||
},
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
const plugin: TuiPluginModule & { id: string } = {
|
||||
id,
|
||||
tui,
|
||||
}
|
||||
|
||||
export default plugin
|
||||
@@ -7,6 +7,7 @@ import SidebarTodo from "../feature-plugins/sidebar/todo"
|
||||
import SidebarFiles from "../feature-plugins/sidebar/files"
|
||||
import SidebarFooter from "../feature-plugins/sidebar/footer"
|
||||
import PluginManager from "../feature-plugins/system/plugins"
|
||||
import SessionV2Debug from "../feature-plugins/system/session-v2"
|
||||
import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
||||
|
||||
export type InternalTuiPlugin = TuiPluginModule & {
|
||||
@@ -24,4 +25,5 @@ export const INTERNAL_TUI_PLUGINS: InternalTuiPlugin[] = [
|
||||
SidebarFiles,
|
||||
SidebarFooter,
|
||||
PluginManager,
|
||||
SessionV2Debug,
|
||||
]
|
||||
|
||||
@@ -6,6 +6,7 @@ import z from "zod"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import { GlobalBus } from "@/bus/global"
|
||||
import { Bus } from "@/bus"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { AsyncQueue } from "@/util/queue"
|
||||
import { Instance } from "../../project/instance"
|
||||
@@ -28,6 +29,7 @@ async function streamEvents(c: Context, subscribe: (q: AsyncQueue<string | null>
|
||||
q.push(
|
||||
JSON.stringify({
|
||||
payload: {
|
||||
id: Bus.createID(),
|
||||
type: "server.connected",
|
||||
properties: {},
|
||||
},
|
||||
@@ -39,6 +41,7 @@ async function streamEvents(c: Context, subscribe: (q: AsyncQueue<string | null>
|
||||
q.push(
|
||||
JSON.stringify({
|
||||
payload: {
|
||||
id: Bus.createID(),
|
||||
type: "server.heartbeat",
|
||||
properties: {},
|
||||
},
|
||||
|
||||
@@ -42,6 +42,7 @@ export const EventRoutes = () =>
|
||||
|
||||
q.push(
|
||||
JSON.stringify({
|
||||
id: Bus.createID(),
|
||||
type: "server.connected",
|
||||
properties: {},
|
||||
}),
|
||||
@@ -50,9 +51,10 @@ export const EventRoutes = () =>
|
||||
// Send heartbeat every 10s to prevent stalled proxy streams.
|
||||
const heartbeat = setInterval(() => {
|
||||
q.push(
|
||||
JSON.stringify({
|
||||
type: "server.heartbeat",
|
||||
properties: {},
|
||||
JSON.stringify({
|
||||
id: Bus.createID(),
|
||||
type: "server.heartbeat",
|
||||
properties: {},
|
||||
}),
|
||||
)
|
||||
}, 10_000)
|
||||
|
||||
@@ -19,6 +19,7 @@ import { SessionApi } from "./groups/session"
|
||||
import { SyncApi } from "./groups/sync"
|
||||
import { TuiApi } from "./groups/tui"
|
||||
import { WorkspaceApi } from "./groups/workspace"
|
||||
import { V2Api } from "./groups/v2"
|
||||
|
||||
// SSE event schemas built from the same BusEvent/SyncEvent registries that
|
||||
// the Hono spec uses, so both specs emit identical Event/SyncEvent components.
|
||||
@@ -40,6 +41,7 @@ export const InstanceHttpApi = HttpApi.make("opencode-instance")
|
||||
.addHttpApi(ProviderApi)
|
||||
.addHttpApi(SessionApi)
|
||||
.addHttpApi(SyncApi)
|
||||
.addHttpApi(V2Api)
|
||||
.addHttpApi(TuiApi)
|
||||
.addHttpApi(WorkspaceApi)
|
||||
|
||||
|
||||
@@ -41,12 +41,12 @@ function eventResponse(bus: Bus.Interface) {
|
||||
const events = bus.subscribeAll().pipe(Stream.takeUntil((event) => event.type === Bus.InstanceDisposed.type))
|
||||
const heartbeat = Stream.tick("10 seconds").pipe(
|
||||
Stream.drop(1),
|
||||
Stream.map(() => ({ type: "server.heartbeat", properties: {} })),
|
||||
Stream.map(() => ({ id: Bus.createID(), type: "server.heartbeat", properties: {} })),
|
||||
)
|
||||
|
||||
log.info("event connected")
|
||||
return HttpServerResponse.stream(
|
||||
Stream.make({ type: "server.connected", properties: {} }).pipe(
|
||||
Stream.make({ id: Bus.createID(), type: "server.connected", properties: {} }).pipe(
|
||||
Stream.concat(events.pipe(Stream.merge(heartbeat, { haltStrategy: "left" }))),
|
||||
Stream.map(eventData),
|
||||
Stream.pipeThroughChannel(Sse.encode()),
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { HttpApi, OpenApi } from "effect/unstable/httpapi"
|
||||
import { MessageGroup } from "./v2/message"
|
||||
import { SessionGroup } from "./v2/session"
|
||||
|
||||
export const V2Api = HttpApi.make("v2")
|
||||
.add(SessionGroup)
|
||||
.add(MessageGroup)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "opencode experimental HttpApi",
|
||||
version: "0.0.1",
|
||||
description: "Experimental HttpApi surface for selected instance routes.",
|
||||
}),
|
||||
)
|
||||
@@ -0,0 +1,69 @@
|
||||
import { SessionID } from "@/session/schema"
|
||||
import { SessionMessage } from "@/v2/session-message"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { Authorization } from "../../middleware/authorization"
|
||||
|
||||
export const MessageGroup = HttpApiGroup.make("v2.message")
|
||||
.add(
|
||||
HttpApiEndpoint.get("messages", "/api/session/:sessionID/message", {
|
||||
params: { sessionID: SessionID },
|
||||
query: Schema.Union([
|
||||
Schema.Struct({
|
||||
limit: Schema.optional(
|
||||
Schema.NumberFromString.check(
|
||||
Schema.isInt(),
|
||||
Schema.isGreaterThanOrEqualTo(1),
|
||||
Schema.isLessThanOrEqualTo(200),
|
||||
),
|
||||
).annotate({
|
||||
description:
|
||||
"Maximum number of messages to return. When omitted, the endpoint returns its default page size.",
|
||||
}),
|
||||
order: Schema.optional(Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")])).annotate({
|
||||
description: "Message order for the first page. Use desc for newest first or asc for oldest first.",
|
||||
}),
|
||||
cursor: Schema.optional(Schema.Never),
|
||||
}),
|
||||
Schema.Struct({
|
||||
limit: Schema.optional(
|
||||
Schema.NumberFromString.check(
|
||||
Schema.isInt(),
|
||||
Schema.isGreaterThanOrEqualTo(1),
|
||||
Schema.isLessThanOrEqualTo(200),
|
||||
),
|
||||
).annotate({
|
||||
description:
|
||||
"Maximum number of messages to return. When omitted, the endpoint returns its default page size.",
|
||||
}),
|
||||
cursor: Schema.String.annotate({
|
||||
description:
|
||||
"Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order.",
|
||||
}),
|
||||
order: Schema.optional(Schema.Never),
|
||||
}),
|
||||
]).annotate({ identifier: "V2SessionMessagesQuery" }),
|
||||
success: Schema.Struct({
|
||||
items: Schema.Array(SessionMessage.Message),
|
||||
cursor: Schema.Struct({
|
||||
previous: Schema.String.pipe(Schema.optional),
|
||||
next: Schema.String.pipe(Schema.optional),
|
||||
}),
|
||||
}).annotate({ identifier: "V2SessionMessagesResponse" }),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.messages",
|
||||
summary: "Get v2 session messages",
|
||||
description:
|
||||
"Retrieve projected v2 messages for a session. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "v2 messages",
|
||||
description: "Experimental v2 message routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(Authorization)
|
||||
@@ -0,0 +1,128 @@
|
||||
import { WorkspaceID } from "@/control-plane/schema"
|
||||
import { SessionID } from "@/session/schema"
|
||||
import { SessionMessage } from "@/v2/session-message"
|
||||
import { Prompt } from "@/v2/session-prompt"
|
||||
import { SessionV2 } from "@/v2/session"
|
||||
import { Schema, SchemaGetter } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
import { Authorization } from "../../middleware/authorization"
|
||||
|
||||
export const SessionGroup = HttpApiGroup.make("v2.session")
|
||||
.add(
|
||||
HttpApiEndpoint.get("sessions", "/api/session", {
|
||||
query: Schema.Union([
|
||||
Schema.Struct({
|
||||
limit: Schema.optional(
|
||||
Schema.NumberFromString.check(
|
||||
Schema.isInt(),
|
||||
Schema.isGreaterThanOrEqualTo(1),
|
||||
Schema.isLessThanOrEqualTo(200),
|
||||
),
|
||||
).annotate({
|
||||
description: "Maximum number of sessions to return. Defaults to the newest 50 sessions.",
|
||||
}),
|
||||
order: Schema.optional(Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")])).annotate({
|
||||
description: "Session order for the first page. Use desc for newest first or asc for oldest first.",
|
||||
}),
|
||||
directory: Schema.String.pipe(Schema.optional),
|
||||
path: Schema.String.pipe(Schema.optional),
|
||||
workspace: WorkspaceID.pipe(Schema.optional),
|
||||
roots: Schema.Literals(["true", "false"])
|
||||
.pipe(
|
||||
Schema.decodeTo(Schema.Boolean, {
|
||||
decode: SchemaGetter.transform((value) => value === "true"),
|
||||
encode: SchemaGetter.transform((value) => (value ? "true" : "false")),
|
||||
}),
|
||||
)
|
||||
.pipe(Schema.optional),
|
||||
start: Schema.NumberFromString.pipe(Schema.optional),
|
||||
search: Schema.String.pipe(Schema.optional),
|
||||
cursor: Schema.optional(Schema.Never),
|
||||
}),
|
||||
Schema.Struct({
|
||||
limit: Schema.optional(
|
||||
Schema.NumberFromString.check(
|
||||
Schema.isInt(),
|
||||
Schema.isGreaterThanOrEqualTo(1),
|
||||
Schema.isLessThanOrEqualTo(200),
|
||||
),
|
||||
).annotate({
|
||||
description: "Maximum number of sessions to return. Defaults to the newest 50 sessions.",
|
||||
}),
|
||||
cursor: Schema.String.annotate({
|
||||
description:
|
||||
"Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order.",
|
||||
}),
|
||||
order: Schema.optional(Schema.Never),
|
||||
directory: Schema.optional(Schema.Never),
|
||||
path: Schema.optional(Schema.Never),
|
||||
workspace: Schema.optional(Schema.Never),
|
||||
roots: Schema.optional(Schema.Never),
|
||||
start: Schema.optional(Schema.Never),
|
||||
search: Schema.optional(Schema.Never),
|
||||
}),
|
||||
]).annotate({ identifier: "V2SessionsQuery" }),
|
||||
success: Schema.Struct({
|
||||
items: Schema.Array(SessionV2.Info),
|
||||
cursor: Schema.Struct({
|
||||
previous: Schema.String.pipe(Schema.optional),
|
||||
next: Schema.String.pipe(Schema.optional),
|
||||
}),
|
||||
}).annotate({ identifier: "V2SessionsResponse" }),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.list",
|
||||
summary: "List v2 sessions",
|
||||
description:
|
||||
"Retrieve sessions in the requested order. Items keep that order across pages; use cursor.next or cursor.previous to move through the ordered list.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("prompt", "/api/session/:sessionID/prompt", {
|
||||
params: { sessionID: SessionID },
|
||||
payload: Schema.Struct({
|
||||
prompt: Prompt,
|
||||
delivery: SessionV2.Delivery.pipe(Schema.optional),
|
||||
}),
|
||||
success: SessionMessage.Message,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.prompt",
|
||||
summary: "Send v2 message",
|
||||
description: "Create a v2 session message and queue it for the agent loop.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("compact", "/api/session/:sessionID/compact", {
|
||||
params: { sessionID: SessionID },
|
||||
success: HttpApiSchema.NoContent,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.compact",
|
||||
summary: "Compact v2 session",
|
||||
description: "Compact a v2 session conversation.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("wait", "/api/session/:sessionID/wait", {
|
||||
params: { sessionID: SessionID },
|
||||
success: HttpApiSchema.NoContent,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.wait",
|
||||
summary: "Wait for v2 session",
|
||||
description: "Wait for a v2 session agent loop to become idle.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "v2",
|
||||
description: "Experimental v2 routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(Authorization)
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Config } from "@/config/config"
|
||||
import { GlobalBus, type GlobalEvent as GlobalBusEvent } from "@/bus/global"
|
||||
import { Bus } from "@/bus"
|
||||
import { Installation } from "@/installation"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
@@ -42,11 +43,11 @@ function eventResponse() {
|
||||
})
|
||||
const heartbeat = Stream.tick("10 seconds").pipe(
|
||||
Stream.drop(1),
|
||||
Stream.map(() => ({ payload: { type: "server.heartbeat", properties: {} } })),
|
||||
Stream.map(() => ({ payload: { id: Bus.createID(), type: "server.heartbeat", properties: {} } })),
|
||||
)
|
||||
|
||||
return HttpServerResponse.stream(
|
||||
Stream.make({ payload: { type: "server.connected", properties: {} } }).pipe(
|
||||
Stream.make({ payload: { id: Bus.createID(), type: "server.connected", properties: {} } }).pipe(
|
||||
Stream.concat(events.pipe(Stream.merge(heartbeat, { haltStrategy: "left" }))),
|
||||
Stream.map(eventData),
|
||||
Stream.pipeThroughChannel(Sse.encode()),
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { SessionV2 } from "@/v2/session"
|
||||
import { Layer } from "effect"
|
||||
import { messageHandlers } from "./v2/message"
|
||||
import { sessionHandlers } from "./v2/session"
|
||||
|
||||
export const v2Handlers = Layer.mergeAll(sessionHandlers, messageHandlers).pipe(Layer.provide(SessionV2.defaultLayer))
|
||||
@@ -0,0 +1,60 @@
|
||||
import { SessionMessage } from "@/v2/session-message"
|
||||
import { SessionV2 } from "@/v2/session"
|
||||
import { Effect, Schema } from "effect"
|
||||
import * as DateTime from "effect/DateTime"
|
||||
import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi"
|
||||
import { InstanceHttpApi } from "../../api"
|
||||
|
||||
const DefaultMessagesLimit = 50
|
||||
|
||||
const Cursor = Schema.Struct({
|
||||
id: SessionMessage.ID,
|
||||
time: Schema.Number,
|
||||
order: Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")]),
|
||||
direction: Schema.Union([Schema.Literal("previous"), Schema.Literal("next")]),
|
||||
})
|
||||
|
||||
const decodeCursor = Schema.decodeUnknownSync(Cursor)
|
||||
|
||||
const cursor = {
|
||||
encode(message: SessionMessage.Message, order: "asc" | "desc", direction: "previous" | "next") {
|
||||
return Buffer.from(
|
||||
JSON.stringify({ id: message.id, time: DateTime.toEpochMillis(message.time.created), order, direction }),
|
||||
).toString("base64url")
|
||||
},
|
||||
decode(input: string) {
|
||||
return decodeCursor(JSON.parse(Buffer.from(input, "base64url").toString("utf8")))
|
||||
},
|
||||
}
|
||||
|
||||
export const messageHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.message", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* SessionV2.Service
|
||||
|
||||
return handlers.handle(
|
||||
"messages",
|
||||
Effect.fn(function* (ctx) {
|
||||
const decoded = yield* Effect.try({
|
||||
try: () => (ctx.query.cursor ? cursor.decode(ctx.query.cursor) : undefined),
|
||||
catch: () => new HttpApiError.BadRequest({}),
|
||||
})
|
||||
const order = decoded?.order ?? ctx.query.order ?? "desc"
|
||||
const messages = yield* session.messages({
|
||||
sessionID: ctx.params.sessionID,
|
||||
limit: ctx.query.limit ?? DefaultMessagesLimit,
|
||||
order,
|
||||
cursor: decoded ? { id: decoded.id, time: decoded.time, direction: decoded.direction } : undefined,
|
||||
})
|
||||
const first = messages[0]
|
||||
const last = messages.at(-1)
|
||||
return {
|
||||
items: messages,
|
||||
cursor: {
|
||||
previous: first ? cursor.encode(first, order, "previous") : undefined,
|
||||
next: last ? cursor.encode(last, order, "next") : undefined,
|
||||
},
|
||||
}
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
@@ -0,0 +1,109 @@
|
||||
import { WorkspaceID } from "@/control-plane/schema"
|
||||
import { SessionV2 } from "@/v2/session"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { HttpApiBuilder, HttpApiError, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { InstanceHttpApi } from "../../api"
|
||||
|
||||
const DefaultSessionsLimit = 50
|
||||
|
||||
const SessionCursor = Schema.Struct({
|
||||
id: SessionV2.Info.fields.id,
|
||||
time: Schema.Number,
|
||||
order: Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")]),
|
||||
direction: Schema.Union([Schema.Literal("previous"), Schema.Literal("next")]),
|
||||
directory: Schema.String.pipe(Schema.optional),
|
||||
path: Schema.String.pipe(Schema.optional),
|
||||
workspaceID: WorkspaceID.pipe(Schema.optional),
|
||||
roots: Schema.Boolean.pipe(Schema.optional),
|
||||
start: Schema.Number.pipe(Schema.optional),
|
||||
search: Schema.String.pipe(Schema.optional),
|
||||
})
|
||||
type SessionCursor = typeof SessionCursor.Type
|
||||
|
||||
const decodeCursor = Schema.decodeUnknownSync(SessionCursor)
|
||||
|
||||
const sessionCursor = {
|
||||
encode(
|
||||
session: SessionV2.Info,
|
||||
order: "asc" | "desc",
|
||||
direction: "previous" | "next",
|
||||
filters: Pick<SessionCursor, "directory" | "path" | "workspaceID" | "roots" | "start" | "search">,
|
||||
) {
|
||||
return Buffer.from(
|
||||
JSON.stringify({ id: session.id, time: session.time.created, order, direction, ...filters }),
|
||||
).toString("base64url")
|
||||
},
|
||||
decode(input: string) {
|
||||
return decodeCursor(JSON.parse(Buffer.from(input, "base64url").toString("utf8")))
|
||||
},
|
||||
}
|
||||
|
||||
export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* SessionV2.Service
|
||||
|
||||
return handlers
|
||||
.handle(
|
||||
"sessions",
|
||||
Effect.fn(function* (ctx) {
|
||||
const decoded = yield* Effect.try({
|
||||
try: () => (ctx.query.cursor ? sessionCursor.decode(ctx.query.cursor) : undefined),
|
||||
catch: () => new HttpApiError.BadRequest({}),
|
||||
})
|
||||
const order = decoded?.order ?? ctx.query.order ?? "desc"
|
||||
const filters = decoded ?? {
|
||||
directory: ctx.query.directory,
|
||||
path: ctx.query.path,
|
||||
workspaceID: ctx.query.workspace ? WorkspaceID.make(ctx.query.workspace) : undefined,
|
||||
roots: ctx.query.roots,
|
||||
start: ctx.query.start,
|
||||
search: ctx.query.search,
|
||||
}
|
||||
const sessions = yield* session.list({
|
||||
limit: ctx.query.limit ?? DefaultSessionsLimit,
|
||||
order,
|
||||
directory: filters.directory,
|
||||
path: filters.path,
|
||||
workspaceID: filters.workspaceID,
|
||||
roots: filters.roots,
|
||||
start: filters.start,
|
||||
search: filters.search,
|
||||
cursor: decoded ? { id: decoded.id, time: decoded.time, direction: decoded.direction } : undefined,
|
||||
})
|
||||
const first = sessions[0]
|
||||
const last = sessions.at(-1)
|
||||
return {
|
||||
items: sessions,
|
||||
cursor: {
|
||||
previous: first ? sessionCursor.encode(first, order, "previous", filters) : undefined,
|
||||
next: last ? sessionCursor.encode(last, order, "next", filters) : undefined,
|
||||
},
|
||||
}
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"prompt",
|
||||
Effect.fn(function* (ctx) {
|
||||
return yield* session.prompt({
|
||||
sessionID: ctx.params.sessionID,
|
||||
prompt: ctx.payload.prompt,
|
||||
delivery: ctx.payload.delivery ?? SessionV2.DefaultDelivery,
|
||||
})
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"compact",
|
||||
Effect.fn(function* (ctx) {
|
||||
yield* session.compact(ctx.params.sessionID)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"wait",
|
||||
Effect.fn(function* (ctx) {
|
||||
yield* session.wait(ctx.params.sessionID)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
@@ -58,6 +58,7 @@ import { questionHandlers } from "./handlers/question"
|
||||
import { sessionHandlers } from "./handlers/session"
|
||||
import { syncHandlers } from "./handlers/sync"
|
||||
import { tuiHandlers } from "./handlers/tui"
|
||||
import { v2Handlers } from "./handlers/v2"
|
||||
import { workspaceHandlers } from "./handlers/workspace"
|
||||
import { instanceContextLayer, instanceRouterMiddleware } from "./middleware/instance-context"
|
||||
import { workspaceRouterMiddleware, workspaceRoutingLayer } from "./middleware/workspace-routing"
|
||||
@@ -109,6 +110,7 @@ const instanceApiRoutes = HttpApiBuilder.layer(InstanceHttpApi).pipe(
|
||||
providerHandlers,
|
||||
sessionHandlers,
|
||||
syncHandlers,
|
||||
v2Handlers,
|
||||
tuiHandlers,
|
||||
workspaceHandlers,
|
||||
]),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { describeRoute, resolver, validator } from "hono-openapi"
|
||||
import { Hono } from "hono"
|
||||
import type { UpgradeWebSocket } from "hono/ws"
|
||||
import { Effect } from "effect"
|
||||
import { Context, Effect } from "effect"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import z from "zod"
|
||||
import { Format } from "@/format"
|
||||
import { TuiRoutes } from "./tui"
|
||||
@@ -24,12 +25,136 @@ import { ExperimentalRoutes } from "./experimental"
|
||||
import { ProviderRoutes } from "./provider"
|
||||
import { EventRoutes } from "./event"
|
||||
import { SyncRoutes } from "./sync"
|
||||
import { V2Routes } from "./v2"
|
||||
import { InstanceMiddleware } from "./middleware"
|
||||
import { jsonRequest } from "./trace"
|
||||
import { ExperimentalHttpApiServer } from "./httpapi/server"
|
||||
import { EventPaths } from "./httpapi/event"
|
||||
import { ExperimentalPaths } from "./httpapi/groups/experimental"
|
||||
import { FilePaths } from "./httpapi/groups/file"
|
||||
import { InstancePaths } from "./httpapi/groups/instance"
|
||||
import { McpPaths } from "./httpapi/groups/mcp"
|
||||
import { PtyPaths } from "./httpapi/groups/pty"
|
||||
import { SessionPaths } from "./httpapi/groups/session"
|
||||
import { SyncPaths } from "./httpapi/groups/sync"
|
||||
import { TuiPaths } from "./httpapi/groups/tui"
|
||||
import { WorkspacePaths } from "./httpapi/groups/workspace"
|
||||
|
||||
export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => {
|
||||
const app = new Hono()
|
||||
|
||||
if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) {
|
||||
const handler = ExperimentalHttpApiServer.webHandler().handler
|
||||
const context = Context.empty() as Context.Context<unknown>
|
||||
app.all("/api/*", (c) => handler(c.req.raw, context))
|
||||
app.get(EventPaths.event, (c) => handler(c.req.raw, context))
|
||||
app.get("/question", (c) => handler(c.req.raw, context))
|
||||
app.post("/question/:requestID/reply", (c) => handler(c.req.raw, context))
|
||||
app.post("/question/:requestID/reject", (c) => handler(c.req.raw, context))
|
||||
app.get("/permission", (c) => handler(c.req.raw, context))
|
||||
app.post("/permission/:requestID/reply", (c) => handler(c.req.raw, context))
|
||||
app.get("/config", (c) => handler(c.req.raw, context))
|
||||
app.patch("/config", (c) => handler(c.req.raw, context))
|
||||
app.get("/config/providers", (c) => handler(c.req.raw, context))
|
||||
app.get(ExperimentalPaths.console, (c) => handler(c.req.raw, context))
|
||||
app.get(ExperimentalPaths.consoleOrgs, (c) => handler(c.req.raw, context))
|
||||
app.post(ExperimentalPaths.consoleSwitch, (c) => handler(c.req.raw, context))
|
||||
app.get(ExperimentalPaths.tool, (c) => handler(c.req.raw, context))
|
||||
app.get(ExperimentalPaths.toolIDs, (c) => handler(c.req.raw, context))
|
||||
app.get(ExperimentalPaths.worktree, (c) => handler(c.req.raw, context))
|
||||
app.post(ExperimentalPaths.worktree, (c) => handler(c.req.raw, context))
|
||||
app.delete(ExperimentalPaths.worktree, (c) => handler(c.req.raw, context))
|
||||
app.post(ExperimentalPaths.worktreeReset, (c) => handler(c.req.raw, context))
|
||||
app.get(ExperimentalPaths.session, (c) => handler(c.req.raw, context))
|
||||
app.get(ExperimentalPaths.resource, (c) => handler(c.req.raw, context))
|
||||
app.get("/provider", (c) => handler(c.req.raw, context))
|
||||
app.get("/provider/auth", (c) => handler(c.req.raw, context))
|
||||
app.post("/provider/:providerID/oauth/authorize", (c) => handler(c.req.raw, context))
|
||||
app.post("/provider/:providerID/oauth/callback", (c) => handler(c.req.raw, context))
|
||||
app.get("/project", (c) => handler(c.req.raw, context))
|
||||
app.get("/project/current", (c) => handler(c.req.raw, context))
|
||||
app.post("/project/git/init", (c) => handler(c.req.raw, context))
|
||||
app.patch("/project/:projectID", (c) => handler(c.req.raw, context))
|
||||
app.get(FilePaths.findText, (c) => handler(c.req.raw, context))
|
||||
app.get(FilePaths.findFile, (c) => handler(c.req.raw, context))
|
||||
app.get(FilePaths.findSymbol, (c) => handler(c.req.raw, context))
|
||||
app.get(FilePaths.list, (c) => handler(c.req.raw, context))
|
||||
app.get(FilePaths.content, (c) => handler(c.req.raw, context))
|
||||
app.get(FilePaths.status, (c) => handler(c.req.raw, context))
|
||||
app.get(InstancePaths.path, (c) => handler(c.req.raw, context))
|
||||
app.post(InstancePaths.dispose, (c) => handler(c.req.raw, context))
|
||||
app.get(InstancePaths.vcs, (c) => handler(c.req.raw, context))
|
||||
app.get(InstancePaths.vcsDiff, (c) => handler(c.req.raw, context))
|
||||
app.get(InstancePaths.command, (c) => handler(c.req.raw, context))
|
||||
app.get(InstancePaths.agent, (c) => handler(c.req.raw, context))
|
||||
app.get(InstancePaths.skill, (c) => handler(c.req.raw, context))
|
||||
app.get(InstancePaths.lsp, (c) => handler(c.req.raw, context))
|
||||
app.get(InstancePaths.formatter, (c) => handler(c.req.raw, context))
|
||||
app.get(McpPaths.status, (c) => handler(c.req.raw, context))
|
||||
app.post(McpPaths.status, (c) => handler(c.req.raw, context))
|
||||
app.post(McpPaths.auth, (c) => handler(c.req.raw, context))
|
||||
app.post(McpPaths.authCallback, (c) => handler(c.req.raw, context))
|
||||
app.post(McpPaths.authAuthenticate, (c) => handler(c.req.raw, context))
|
||||
app.delete(McpPaths.auth, (c) => handler(c.req.raw, context))
|
||||
app.post(McpPaths.connect, (c) => handler(c.req.raw, context))
|
||||
app.post(McpPaths.disconnect, (c) => handler(c.req.raw, context))
|
||||
app.post(SyncPaths.start, (c) => handler(c.req.raw, context))
|
||||
app.post(SyncPaths.replay, (c) => handler(c.req.raw, context))
|
||||
app.post(SyncPaths.history, (c) => handler(c.req.raw, context))
|
||||
app.get(PtyPaths.list, (c) => handler(c.req.raw, context))
|
||||
app.post(PtyPaths.create, (c) => handler(c.req.raw, context))
|
||||
app.get(PtyPaths.get, (c) => handler(c.req.raw, context))
|
||||
app.put(PtyPaths.update, (c) => handler(c.req.raw, context))
|
||||
app.delete(PtyPaths.remove, (c) => handler(c.req.raw, context))
|
||||
app.get(PtyPaths.connect, (c) => handler(c.req.raw, context))
|
||||
app.get(SessionPaths.list, (c) => handler(c.req.raw, context))
|
||||
app.get(SessionPaths.status, (c) => handler(c.req.raw, context))
|
||||
app.get(SessionPaths.get, (c) => handler(c.req.raw, context))
|
||||
app.get(SessionPaths.children, (c) => handler(c.req.raw, context))
|
||||
app.get(SessionPaths.todo, (c) => handler(c.req.raw, context))
|
||||
app.get(SessionPaths.diff, (c) => handler(c.req.raw, context))
|
||||
app.get(SessionPaths.messages, (c) => handler(c.req.raw, context))
|
||||
app.get(SessionPaths.message, (c) => handler(c.req.raw, context))
|
||||
app.post(SessionPaths.create, (c) => handler(c.req.raw, context))
|
||||
app.delete(SessionPaths.remove, (c) => handler(c.req.raw, context))
|
||||
app.patch(SessionPaths.update, (c) => handler(c.req.raw, context))
|
||||
app.post(SessionPaths.init, (c) => handler(c.req.raw, context))
|
||||
app.post(SessionPaths.fork, (c) => handler(c.req.raw, context))
|
||||
app.post(SessionPaths.abort, (c) => handler(c.req.raw, context))
|
||||
app.post(SessionPaths.share, (c) => handler(c.req.raw, context))
|
||||
app.delete(SessionPaths.share, (c) => handler(c.req.raw, context))
|
||||
app.post(SessionPaths.summarize, (c) => handler(c.req.raw, context))
|
||||
app.post(SessionPaths.prompt, (c) => handler(c.req.raw, context))
|
||||
app.post(SessionPaths.promptAsync, (c) => handler(c.req.raw, context))
|
||||
app.post(SessionPaths.command, (c) => handler(c.req.raw, context))
|
||||
app.post(SessionPaths.shell, (c) => handler(c.req.raw, context))
|
||||
app.post(SessionPaths.revert, (c) => handler(c.req.raw, context))
|
||||
app.post(SessionPaths.unrevert, (c) => handler(c.req.raw, context))
|
||||
app.post(SessionPaths.permissions, (c) => handler(c.req.raw, context))
|
||||
app.delete(SessionPaths.deleteMessage, (c) => handler(c.req.raw, context))
|
||||
app.delete(SessionPaths.deletePart, (c) => handler(c.req.raw, context))
|
||||
app.patch(SessionPaths.updatePart, (c) => handler(c.req.raw, context))
|
||||
app.post(TuiPaths.appendPrompt, (c) => handler(c.req.raw, context))
|
||||
app.post(TuiPaths.openHelp, (c) => handler(c.req.raw, context))
|
||||
app.post(TuiPaths.openSessions, (c) => handler(c.req.raw, context))
|
||||
app.post(TuiPaths.openThemes, (c) => handler(c.req.raw, context))
|
||||
app.post(TuiPaths.openModels, (c) => handler(c.req.raw, context))
|
||||
app.post(TuiPaths.submitPrompt, (c) => handler(c.req.raw, context))
|
||||
app.post(TuiPaths.clearPrompt, (c) => handler(c.req.raw, context))
|
||||
app.post(TuiPaths.executeCommand, (c) => handler(c.req.raw, context))
|
||||
app.post(TuiPaths.showToast, (c) => handler(c.req.raw, context))
|
||||
app.post(TuiPaths.publish, (c) => handler(c.req.raw, context))
|
||||
app.post(TuiPaths.selectSession, (c) => handler(c.req.raw, context))
|
||||
app.get(TuiPaths.controlNext, (c) => handler(c.req.raw, context))
|
||||
app.post(TuiPaths.controlResponse, (c) => handler(c.req.raw, context))
|
||||
app.get(WorkspacePaths.adapters, (c) => handler(c.req.raw, context))
|
||||
app.post(WorkspacePaths.list, (c) => handler(c.req.raw, context))
|
||||
app.get(WorkspacePaths.list, (c) => handler(c.req.raw, context))
|
||||
app.get(WorkspacePaths.status, (c) => handler(c.req.raw, context))
|
||||
app.delete(WorkspacePaths.remove, (c) => handler(c.req.raw, context))
|
||||
app.post(WorkspacePaths.sessionRestore, (c) => handler(c.req.raw, context))
|
||||
}
|
||||
|
||||
return app
|
||||
.route("/project", ProjectRoutes())
|
||||
.route("/pty", PtyRoutes(upgrade))
|
||||
@@ -40,6 +165,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => {
|
||||
.route("/question", QuestionRoutes())
|
||||
.route("/provider", ProviderRoutes())
|
||||
.route("/sync", SyncRoutes())
|
||||
.route("/api", V2Routes())
|
||||
.route("/", FileRoutes())
|
||||
.route("/", EventRoutes())
|
||||
.route("/mcp", McpRoutes())
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
import { WorkspaceID } from "@/control-plane/schema"
|
||||
import { SessionID } from "@/session/schema"
|
||||
import { SessionMessage } from "@/v2/session-message"
|
||||
import { SessionV2 } from "@/v2/session"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { lazy } from "@/util/lazy"
|
||||
import { Effect, Schema } from "effect"
|
||||
import * as DateTime from "effect/DateTime"
|
||||
import { Hono } from "hono"
|
||||
import { describeRoute, resolver, validator } from "hono-openapi"
|
||||
import { HTTPException } from "hono/http-exception"
|
||||
import z from "zod"
|
||||
import { errors } from "../../error"
|
||||
import { jsonRequest } from "./trace"
|
||||
|
||||
const DefaultMessagesLimit = 50
|
||||
const DefaultSessionsLimit = 50
|
||||
|
||||
const SessionCursor = Schema.Struct({
|
||||
id: SessionID,
|
||||
time: Schema.Number,
|
||||
order: Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")]),
|
||||
direction: Schema.Union([Schema.Literal("previous"), Schema.Literal("next")]),
|
||||
directory: Schema.String.pipe(Schema.optional),
|
||||
path: Schema.String.pipe(Schema.optional),
|
||||
workspaceID: WorkspaceID.pipe(Schema.optional),
|
||||
roots: Schema.Boolean.pipe(Schema.optional),
|
||||
start: Schema.Number.pipe(Schema.optional),
|
||||
search: Schema.String.pipe(Schema.optional),
|
||||
})
|
||||
type SessionCursor = typeof SessionCursor.Type
|
||||
|
||||
const SessionsResponse = Schema.Struct({
|
||||
items: Schema.Array(SessionV2.Info),
|
||||
cursor: Schema.Struct({
|
||||
previous: Schema.String.pipe(Schema.optional),
|
||||
next: Schema.String.pipe(Schema.optional),
|
||||
}),
|
||||
}).annotate({ identifier: "V2SessionsResponse" })
|
||||
|
||||
const Cursor = Schema.Struct({
|
||||
id: SessionMessage.ID,
|
||||
time: Schema.Number,
|
||||
order: Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")]),
|
||||
direction: Schema.Union([Schema.Literal("previous"), Schema.Literal("next")]),
|
||||
})
|
||||
|
||||
const MessagesResponse = Schema.Struct({
|
||||
items: Schema.Array(SessionMessage.Message),
|
||||
cursor: Schema.Struct({
|
||||
previous: Schema.String.pipe(Schema.optional),
|
||||
next: Schema.String.pipe(Schema.optional),
|
||||
}),
|
||||
}).annotate({ identifier: "V2SessionMessagesResponse" })
|
||||
|
||||
const decodeCursor = Schema.decodeUnknownSync(Cursor)
|
||||
const decodeSessionCursor = Schema.decodeUnknownSync(SessionCursor)
|
||||
|
||||
const sessionCursor = {
|
||||
encode(
|
||||
session: SessionV2.Info,
|
||||
order: "asc" | "desc",
|
||||
direction: "previous" | "next",
|
||||
filters: Pick<SessionCursor, "directory" | "path" | "workspaceID" | "roots" | "start" | "search">,
|
||||
) {
|
||||
return Buffer.from(
|
||||
JSON.stringify({ id: session.id, time: session.time.created, order, direction, ...filters }),
|
||||
).toString("base64url")
|
||||
},
|
||||
decode(input: string) {
|
||||
return decodeSessionCursor(JSON.parse(Buffer.from(input, "base64url").toString("utf8")))
|
||||
},
|
||||
}
|
||||
|
||||
const cursor = {
|
||||
encode(message: SessionMessage.Message, order: "asc" | "desc", direction: "previous" | "next") {
|
||||
return Buffer.from(
|
||||
JSON.stringify({ id: message.id, time: DateTime.toEpochMillis(message.time.created), order, direction }),
|
||||
).toString("base64url")
|
||||
},
|
||||
decode(input: string) {
|
||||
return decodeCursor(JSON.parse(Buffer.from(input, "base64url").toString("utf8")))
|
||||
},
|
||||
}
|
||||
|
||||
export const V2Routes = lazy(() =>
|
||||
new Hono()
|
||||
.get(
|
||||
"/session",
|
||||
describeRoute({
|
||||
summary: "List v2 sessions",
|
||||
description:
|
||||
"Retrieve sessions in the requested order. Items keep that order across pages; use cursor.next or cursor.previous to move through the ordered list.",
|
||||
operationId: "v2.session.list",
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of v2 sessions",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(zod(SessionsResponse)),
|
||||
},
|
||||
},
|
||||
},
|
||||
...errors(400),
|
||||
},
|
||||
}),
|
||||
validator(
|
||||
"query",
|
||||
z.object({
|
||||
limit: z.coerce.number().int().min(1).max(200).optional(),
|
||||
cursor: z.string().optional(),
|
||||
order: z.enum(["asc", "desc"]).optional(),
|
||||
directory: z.string().optional(),
|
||||
path: z.string().optional(),
|
||||
workspace: WorkspaceID.zod.optional(),
|
||||
roots: z
|
||||
.enum(["true", "false"])
|
||||
.transform((value) => value === "true")
|
||||
.optional(),
|
||||
start: z.coerce.number().optional(),
|
||||
search: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const query = c.req.valid("query")
|
||||
const decoded = (() => {
|
||||
try {
|
||||
return query.cursor ? sessionCursor.decode(query.cursor) : undefined
|
||||
} catch {
|
||||
throw new HTTPException(400)
|
||||
}
|
||||
})()
|
||||
const order = decoded?.order ?? query.order ?? "desc"
|
||||
const filters = decoded ?? {
|
||||
directory: query.directory,
|
||||
path: query.path,
|
||||
workspaceID: query.workspace,
|
||||
roots: query.roots,
|
||||
start: query.start,
|
||||
search: query.search,
|
||||
}
|
||||
return jsonRequest("V2Routes.sessions", c, function* () {
|
||||
return yield* Effect.gen(function* () {
|
||||
const session = yield* SessionV2.Service
|
||||
const sessions = yield* session.list({
|
||||
limit: query.limit ?? DefaultSessionsLimit,
|
||||
order,
|
||||
directory: filters.directory,
|
||||
path: filters.path,
|
||||
workspaceID: filters.workspaceID,
|
||||
roots: filters.roots,
|
||||
start: filters.start,
|
||||
search: filters.search,
|
||||
cursor: decoded ? { id: decoded.id, time: decoded.time, direction: decoded.direction } : undefined,
|
||||
})
|
||||
const first = sessions[0]
|
||||
const last = sessions.at(-1)
|
||||
return {
|
||||
items: sessions,
|
||||
cursor: {
|
||||
previous: first ? sessionCursor.encode(first, order, "previous", filters) : undefined,
|
||||
next: last ? sessionCursor.encode(last, order, "next", filters) : undefined,
|
||||
},
|
||||
}
|
||||
}).pipe(Effect.provide(SessionV2.defaultLayer))
|
||||
})
|
||||
},
|
||||
)
|
||||
.get(
|
||||
"/session/:sessionID/message",
|
||||
describeRoute({
|
||||
summary: "Get v2 session messages",
|
||||
description: "Retrieve projected v2 messages for a session directly from the message database.",
|
||||
operationId: "v2.session.messages",
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of v2 session messages",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(zod(MessagesResponse)),
|
||||
},
|
||||
},
|
||||
},
|
||||
...errors(400, 404),
|
||||
},
|
||||
}),
|
||||
validator("param", z.object({ sessionID: SessionID.zod })),
|
||||
validator(
|
||||
"query",
|
||||
z.object({
|
||||
limit: z.coerce.number().int().min(1).max(200).optional(),
|
||||
cursor: z.string().optional(),
|
||||
order: z.enum(["asc", "desc"]).optional(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const sessionID = c.req.valid("param").sessionID
|
||||
const query = c.req.valid("query")
|
||||
const decoded = (() => {
|
||||
try {
|
||||
return query.cursor ? cursor.decode(query.cursor) : undefined
|
||||
} catch {
|
||||
throw new HTTPException(400)
|
||||
}
|
||||
})()
|
||||
const order = decoded?.order ?? query.order ?? "desc"
|
||||
return jsonRequest("V2Routes.messages", c, function* () {
|
||||
return yield* Effect.gen(function* () {
|
||||
const session = yield* SessionV2.Service
|
||||
const messages = yield* session.messages({
|
||||
sessionID,
|
||||
limit: query.limit ?? DefaultMessagesLimit,
|
||||
order,
|
||||
cursor: decoded ? { id: decoded.id, time: decoded.time, direction: decoded.direction } : undefined,
|
||||
})
|
||||
const first = messages[0]
|
||||
const last = messages.at(-1)
|
||||
return {
|
||||
items: messages,
|
||||
cursor: {
|
||||
previous: first ? cursor.encode(first, order, "previous") : undefined,
|
||||
next: last ? cursor.encode(last, order, "next") : undefined,
|
||||
},
|
||||
}
|
||||
}).pipe(Effect.provide(SessionV2.defaultLayer))
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -14,10 +14,13 @@ import { Config } from "@/config/config"
|
||||
import { NotFoundError } from "@/storage/storage"
|
||||
import { ModelID, ProviderID } from "@/provider/schema"
|
||||
import { Effect, Layer, Context, Schema } from "effect"
|
||||
import * as DateTime from "effect/DateTime"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { isOverflow as overflow, usable } from "./overflow"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { fn } from "@/util/fn"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import { SessionEvent } from "@/v2/session-event"
|
||||
|
||||
const log = Log.create({ service: "session.compaction" })
|
||||
|
||||
@@ -556,7 +559,21 @@ export const layer: Layer.Layer<
|
||||
}
|
||||
|
||||
if (processor.message.error) return "stop"
|
||||
if (result === "continue") yield* bus.publish(Event.Compacted, { sessionID: input.sessionID })
|
||||
if (result === "continue") {
|
||||
const summary = summaryText(
|
||||
(yield* session.messages({ sessionID: input.sessionID })).find((item) => item.info.id === msg.id) ?? {
|
||||
info: msg,
|
||||
parts: [],
|
||||
},
|
||||
)
|
||||
SyncEvent.run(SessionEvent.Compaction.Ended.Sync, {
|
||||
sessionID: input.sessionID,
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
text: summary ?? "",
|
||||
include: selected.tail_start_id,
|
||||
})
|
||||
yield* bus.publish(Event.Compacted, { sessionID: input.sessionID })
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
@@ -583,6 +600,11 @@ export const layer: Layer.Layer<
|
||||
auto: input.auto,
|
||||
overflow: input.overflow,
|
||||
})
|
||||
SyncEvent.run(SessionEvent.Compaction.Started.Sync, {
|
||||
sessionID: input.sessionID,
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
reason: input.auto ? "auto" : "manual",
|
||||
})
|
||||
})
|
||||
|
||||
return Service.of({
|
||||
|
||||
@@ -20,6 +20,9 @@ import { Question } from "@/question"
|
||||
import { errorMessage } from "@/util/error"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { isRecord } from "@/util/record"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import { SessionEvent } from "@/v2/session-event"
|
||||
import * as DateTime from "effect/DateTime"
|
||||
|
||||
const DOOM_LOOP_THRESHOLD = 3
|
||||
const log = Log.create({ service: "session.processor" })
|
||||
@@ -221,6 +224,12 @@ export const layer: Layer.Layer<
|
||||
|
||||
case "reasoning-start":
|
||||
if (value.id in ctx.reasoningMap) return
|
||||
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
|
||||
SyncEvent.run(SessionEvent.Reasoning.Started.Sync, {
|
||||
sessionID: ctx.sessionID,
|
||||
reasoningID: value.id,
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
ctx.reasoningMap[value.id] = {
|
||||
id: PartID.ascending(),
|
||||
messageID: ctx.assistantMessage.id,
|
||||
@@ -235,6 +244,13 @@ export const layer: Layer.Layer<
|
||||
|
||||
case "reasoning-delta":
|
||||
if (!(value.id in ctx.reasoningMap)) return
|
||||
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
|
||||
SyncEvent.run(SessionEvent.Reasoning.Delta.Sync, {
|
||||
sessionID: ctx.sessionID,
|
||||
reasoningID: value.id,
|
||||
delta: value.text,
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
ctx.reasoningMap[value.id].text += value.text
|
||||
if (value.providerMetadata) ctx.reasoningMap[value.id].metadata = value.providerMetadata
|
||||
yield* session.updatePartDelta({
|
||||
@@ -248,6 +264,13 @@ export const layer: Layer.Layer<
|
||||
|
||||
case "reasoning-end":
|
||||
if (!(value.id in ctx.reasoningMap)) return
|
||||
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
|
||||
SyncEvent.run(SessionEvent.Reasoning.Ended.Sync, {
|
||||
sessionID: ctx.sessionID,
|
||||
reasoningID: value.id,
|
||||
text: ctx.reasoningMap[value.id].text,
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
// oxlint-disable-next-line no-self-assign -- reactivity trigger
|
||||
ctx.reasoningMap[value.id].text = ctx.reasoningMap[value.id].text
|
||||
ctx.reasoningMap[value.id].time = { ...ctx.reasoningMap[value.id].time, end: Date.now() }
|
||||
@@ -260,6 +283,13 @@ export const layer: Layer.Layer<
|
||||
if (ctx.assistantMessage.summary) {
|
||||
throw new Error(`Tool call not allowed while generating summary: ${value.toolName}`)
|
||||
}
|
||||
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
|
||||
SyncEvent.run(SessionEvent.Tool.Input.Started.Sync, {
|
||||
sessionID: ctx.sessionID,
|
||||
callID: value.id,
|
||||
name: value.toolName,
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
const part = yield* session.updatePart({
|
||||
id: ctx.toolcalls[value.id]?.partID ?? PartID.ascending(),
|
||||
messageID: ctx.assistantMessage.id,
|
||||
@@ -281,13 +311,34 @@ export const layer: Layer.Layer<
|
||||
case "tool-input-delta":
|
||||
return
|
||||
|
||||
case "tool-input-end":
|
||||
case "tool-input-end": {
|
||||
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
|
||||
SyncEvent.run(SessionEvent.Tool.Input.Ended.Sync, {
|
||||
sessionID: ctx.sessionID,
|
||||
callID: value.id,
|
||||
text: "",
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
case "tool-call": {
|
||||
if (ctx.assistantMessage.summary) {
|
||||
throw new Error(`Tool call not allowed while generating summary: ${value.toolName}`)
|
||||
}
|
||||
const toolCall = yield* readToolCall(value.toolCallId)
|
||||
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
|
||||
SyncEvent.run(SessionEvent.Tool.Called.Sync, {
|
||||
sessionID: ctx.sessionID,
|
||||
callID: value.toolCallId,
|
||||
tool: value.toolName,
|
||||
input: value.input,
|
||||
provider: {
|
||||
executed: toolCall?.part.metadata?.providerExecuted === true,
|
||||
...(value.providerMetadata ? { metadata: value.providerMetadata } : {}),
|
||||
},
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
yield* updateToolCall(value.toolCallId, (match) => ({
|
||||
...match,
|
||||
tool: value.toolName,
|
||||
@@ -331,11 +382,48 @@ export const layer: Layer.Layer<
|
||||
}
|
||||
|
||||
case "tool-result": {
|
||||
const toolCall = yield* readToolCall(value.toolCallId)
|
||||
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
|
||||
SyncEvent.run(SessionEvent.Tool.Success.Sync, {
|
||||
sessionID: ctx.sessionID,
|
||||
callID: value.toolCallId,
|
||||
structured: value.output.metadata,
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: value.output.output,
|
||||
},
|
||||
...(value.output.attachments?.map((item: MessageV2.FilePart) => ({
|
||||
type: "file",
|
||||
uri: item.url,
|
||||
mime: item.mime,
|
||||
name: item.filename,
|
||||
})) ?? []),
|
||||
],
|
||||
provider: {
|
||||
executed: toolCall?.part.metadata?.providerExecuted === true,
|
||||
},
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
yield* completeToolCall(value.toolCallId, value.output)
|
||||
return
|
||||
}
|
||||
|
||||
case "tool-error": {
|
||||
const toolCall = yield* readToolCall(value.toolCallId)
|
||||
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
|
||||
SyncEvent.run(SessionEvent.Tool.Error.Sync, {
|
||||
sessionID: ctx.sessionID,
|
||||
callID: value.toolCallId,
|
||||
error: {
|
||||
type: "unknown",
|
||||
message: errorMessage(value.error),
|
||||
},
|
||||
provider: {
|
||||
executed: toolCall?.part.metadata?.providerExecuted === true,
|
||||
},
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
yield* failToolCall(value.toolCallId, value.error)
|
||||
return
|
||||
}
|
||||
@@ -345,6 +433,20 @@ export const layer: Layer.Layer<
|
||||
|
||||
case "start-step":
|
||||
if (!ctx.snapshot) ctx.snapshot = yield* snapshot.track()
|
||||
if (!ctx.assistantMessage.summary) {
|
||||
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
|
||||
SyncEvent.run(SessionEvent.Step.Started.Sync, {
|
||||
sessionID: ctx.sessionID,
|
||||
agent: input.assistantMessage.agent,
|
||||
model: {
|
||||
id: ctx.model.id,
|
||||
providerID: ctx.model.providerID,
|
||||
variant: input.assistantMessage.variant,
|
||||
},
|
||||
snapshot: ctx.snapshot,
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
}
|
||||
yield* session.updatePart({
|
||||
id: PartID.ascending(),
|
||||
messageID: ctx.assistantMessage.id,
|
||||
@@ -355,18 +457,30 @@ export const layer: Layer.Layer<
|
||||
return
|
||||
|
||||
case "finish-step": {
|
||||
const completedSnapshot = yield* snapshot.track()
|
||||
const usage = Session.getUsage({
|
||||
model: ctx.model,
|
||||
usage: value.usage,
|
||||
metadata: value.providerMetadata,
|
||||
})
|
||||
if (!ctx.assistantMessage.summary) {
|
||||
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
|
||||
SyncEvent.run(SessionEvent.Step.Ended.Sync, {
|
||||
sessionID: ctx.sessionID,
|
||||
finish: value.finishReason,
|
||||
cost: usage.cost,
|
||||
tokens: usage.tokens,
|
||||
snapshot: completedSnapshot,
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
}
|
||||
ctx.assistantMessage.finish = value.finishReason
|
||||
ctx.assistantMessage.cost += usage.cost
|
||||
ctx.assistantMessage.tokens = usage.tokens
|
||||
yield* session.updatePart({
|
||||
id: PartID.ascending(),
|
||||
reason: value.finishReason,
|
||||
snapshot: yield* snapshot.track(),
|
||||
snapshot: completedSnapshot,
|
||||
messageID: ctx.assistantMessage.id,
|
||||
sessionID: ctx.assistantMessage.sessionID,
|
||||
type: "step-finish",
|
||||
@@ -404,6 +518,13 @@ export const layer: Layer.Layer<
|
||||
}
|
||||
|
||||
case "text-start":
|
||||
if (!ctx.assistantMessage.summary) {
|
||||
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
|
||||
SyncEvent.run(SessionEvent.Text.Started.Sync, {
|
||||
sessionID: ctx.sessionID,
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
}
|
||||
ctx.currentText = {
|
||||
id: PartID.ascending(),
|
||||
messageID: ctx.assistantMessage.id,
|
||||
@@ -418,6 +539,13 @@ export const layer: Layer.Layer<
|
||||
|
||||
case "text-delta":
|
||||
if (!ctx.currentText) return
|
||||
if (ctx.assistantMessage.summary) {
|
||||
SyncEvent.run(SessionEvent.Compaction.Delta.Sync, {
|
||||
sessionID: ctx.sessionID,
|
||||
text: value.text,
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
}
|
||||
ctx.currentText.text += value.text
|
||||
if (value.providerMetadata) ctx.currentText.metadata = value.providerMetadata
|
||||
yield* session.updatePartDelta({
|
||||
@@ -442,6 +570,14 @@ export const layer: Layer.Layer<
|
||||
},
|
||||
{ text: ctx.currentText.text },
|
||||
)).text
|
||||
if (!ctx.assistantMessage.summary) {
|
||||
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
|
||||
SyncEvent.run(SessionEvent.Text.Ended.Sync, {
|
||||
sessionID: ctx.sessionID,
|
||||
text: ctx.currentText.text,
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
}
|
||||
{
|
||||
const end = Date.now()
|
||||
ctx.currentText.time = { start: ctx.currentText.time?.start ?? end, end }
|
||||
@@ -568,13 +704,24 @@ export const layer: Layer.Layer<
|
||||
Effect.retry(
|
||||
SessionRetry.policy({
|
||||
parse,
|
||||
set: (info) =>
|
||||
status.set(ctx.sessionID, {
|
||||
set: (info) => {
|
||||
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
|
||||
SyncEvent.run(SessionEvent.Retried.Sync, {
|
||||
sessionID: ctx.sessionID,
|
||||
attempt: info.attempt,
|
||||
error: {
|
||||
message: info.message,
|
||||
isRetryable: true,
|
||||
},
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
})
|
||||
return status.set(ctx.sessionID, {
|
||||
type: "retry",
|
||||
attempt: info.attempt,
|
||||
message: info.message,
|
||||
next: info.next,
|
||||
}),
|
||||
})
|
||||
},
|
||||
}),
|
||||
),
|
||||
Effect.catch(halt),
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import { and, desc, eq } from "@/storage/db"
|
||||
import type { Database } from "@/storage/db"
|
||||
import { SessionMessage } from "@/v2/session-message"
|
||||
import { SessionMessageUpdater } from "@/v2/session-message-updater"
|
||||
import { SessionEvent } from "@/v2/session-event"
|
||||
import * as DateTime from "effect/DateTime"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import { SessionMessageTable, SessionTable } from "./session.sql"
|
||||
import type { SessionID } from "./schema"
|
||||
import { Schema } from "effect"
|
||||
|
||||
const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Message)
|
||||
type SessionMessageData = NonNullable<(typeof SessionMessageTable.$inferInsert)["data"]>
|
||||
|
||||
function encodeDateTimes(value: unknown): unknown {
|
||||
if (DateTime.isDateTime(value)) return DateTime.toEpochMillis(value)
|
||||
if (Array.isArray(value)) return value.map(encodeDateTimes)
|
||||
if (typeof value === "object" && value !== null) {
|
||||
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, encodeDateTimes(item)]))
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function encodeMessageData(value: unknown): SessionMessageData {
|
||||
return encodeDateTimes(value) as SessionMessageData
|
||||
}
|
||||
|
||||
function sqlite(db: Database.TxOrDb, sessionID: SessionID): SessionMessageUpdater.Adapter<void> {
|
||||
return {
|
||||
getCurrentAssistant() {
|
||||
return db
|
||||
.select()
|
||||
.from(SessionMessageTable)
|
||||
.where(and(eq(SessionMessageTable.session_id, sessionID), eq(SessionMessageTable.type, "assistant")))
|
||||
.orderBy(desc(SessionMessageTable.id))
|
||||
.all()
|
||||
.map((row) => decodeMessage({ ...row.data, id: row.id, type: row.type }))
|
||||
.find((message): message is SessionMessage.Assistant => message.type === "assistant" && !message.time.completed)
|
||||
},
|
||||
getCurrentCompaction() {
|
||||
return db
|
||||
.select()
|
||||
.from(SessionMessageTable)
|
||||
.where(and(eq(SessionMessageTable.session_id, sessionID), eq(SessionMessageTable.type, "compaction")))
|
||||
.orderBy(desc(SessionMessageTable.id))
|
||||
.all()
|
||||
.map((row) => decodeMessage({ ...row.data, id: row.id, type: row.type }))
|
||||
.find((message): message is SessionMessage.Compaction => message.type === "compaction")
|
||||
},
|
||||
updateAssistant(assistant) {
|
||||
const { id, type, ...data } = assistant
|
||||
db.update(SessionMessageTable)
|
||||
.set({ data: encodeMessageData(data) })
|
||||
.where(
|
||||
and(
|
||||
eq(SessionMessageTable.id, id),
|
||||
eq(SessionMessageTable.session_id, sessionID),
|
||||
eq(SessionMessageTable.type, type),
|
||||
),
|
||||
)
|
||||
.run()
|
||||
},
|
||||
updateCompaction(compaction) {
|
||||
const { id, type, ...data } = compaction
|
||||
db.update(SessionMessageTable)
|
||||
.set({ data: encodeMessageData(data) })
|
||||
.where(
|
||||
and(
|
||||
eq(SessionMessageTable.id, id),
|
||||
eq(SessionMessageTable.session_id, sessionID),
|
||||
eq(SessionMessageTable.type, type),
|
||||
),
|
||||
)
|
||||
.run()
|
||||
},
|
||||
appendMessage(message) {
|
||||
const { id, type, ...data } = message
|
||||
db.insert(SessionMessageTable)
|
||||
.values([
|
||||
{
|
||||
id,
|
||||
session_id: sessionID,
|
||||
type,
|
||||
time_created: DateTime.toEpochMillis(message.time.created),
|
||||
data: encodeMessageData(data),
|
||||
},
|
||||
])
|
||||
.run()
|
||||
},
|
||||
finish() {},
|
||||
}
|
||||
}
|
||||
|
||||
function update(db: Database.TxOrDb, event: SessionEvent.Event) {
|
||||
SessionMessageUpdater.update(sqlite(db, event.data.sessionID), event)
|
||||
}
|
||||
|
||||
export default [
|
||||
SyncEvent.project(SessionEvent.AgentSwitched.Sync, (db, data, event) => {
|
||||
db.update(SessionTable)
|
||||
.set({
|
||||
agent: data.agent,
|
||||
time_updated: DateTime.toEpochMillis(data.timestamp),
|
||||
})
|
||||
.where(eq(SessionTable.id, data.sessionID))
|
||||
.run()
|
||||
update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.agent.switched", data })
|
||||
}),
|
||||
SyncEvent.project(SessionEvent.ModelSwitched.Sync, (db, data, event) => {
|
||||
db.update(SessionTable)
|
||||
.set({
|
||||
model: {
|
||||
id: data.id,
|
||||
providerID: data.providerID,
|
||||
variant: data.variant,
|
||||
},
|
||||
time_updated: DateTime.toEpochMillis(data.timestamp),
|
||||
})
|
||||
.where(eq(SessionTable.id, data.sessionID))
|
||||
.run()
|
||||
update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.model.switched", data })
|
||||
}),
|
||||
SyncEvent.project(SessionEvent.Prompted.Sync, (db, data, event) => {
|
||||
update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.prompted", data })
|
||||
}),
|
||||
SyncEvent.project(SessionEvent.Synthetic.Sync, (db, data, event) => {
|
||||
update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.synthetic", data })
|
||||
}),
|
||||
SyncEvent.project(SessionEvent.Step.Started.Sync, (db, data, event) => {
|
||||
update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.step.started", data })
|
||||
}),
|
||||
SyncEvent.project(SessionEvent.Step.Ended.Sync, (db, data, event) => {
|
||||
update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.step.ended", data })
|
||||
}),
|
||||
SyncEvent.project(SessionEvent.Text.Started.Sync, (db, data, event) => {
|
||||
update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.text.started", data })
|
||||
}),
|
||||
SyncEvent.project(SessionEvent.Text.Delta.Sync, () => {}),
|
||||
SyncEvent.project(SessionEvent.Text.Ended.Sync, (db, data, event) => {
|
||||
update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.text.ended", data })
|
||||
}),
|
||||
SyncEvent.project(SessionEvent.Tool.Input.Started.Sync, (db, data, event) => {
|
||||
update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.tool.input.started", data })
|
||||
}),
|
||||
SyncEvent.project(SessionEvent.Tool.Input.Delta.Sync, () => {}),
|
||||
SyncEvent.project(SessionEvent.Tool.Input.Ended.Sync, (db, data, event) => {
|
||||
update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.tool.input.ended", data })
|
||||
}),
|
||||
SyncEvent.project(SessionEvent.Tool.Called.Sync, (db, data, event) => {
|
||||
update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.tool.called", data })
|
||||
}),
|
||||
SyncEvent.project(SessionEvent.Tool.Success.Sync, (db, data, event) => {
|
||||
update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.tool.success", data })
|
||||
}),
|
||||
SyncEvent.project(SessionEvent.Tool.Error.Sync, (db, data, event) => {
|
||||
update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.tool.error", data })
|
||||
}),
|
||||
SyncEvent.project(SessionEvent.Reasoning.Started.Sync, (db, data, event) => {
|
||||
update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.reasoning.started", data })
|
||||
}),
|
||||
SyncEvent.project(SessionEvent.Reasoning.Delta.Sync, () => {}),
|
||||
SyncEvent.project(SessionEvent.Reasoning.Ended.Sync, (db, data, event) => {
|
||||
update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.reasoning.ended", data })
|
||||
}),
|
||||
SyncEvent.project(SessionEvent.Retried.Sync, (db, data, event) => {
|
||||
update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.retried", data })
|
||||
}),
|
||||
SyncEvent.project(SessionEvent.Compaction.Started.Sync, (db, data, event) => {
|
||||
update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.compaction.started", data })
|
||||
}),
|
||||
SyncEvent.project(SessionEvent.Compaction.Delta.Sync, (db, data, event) => {
|
||||
update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.compaction.delta", data })
|
||||
}),
|
||||
SyncEvent.project(SessionEvent.Compaction.Ended.Sync, (db, data, event) => {
|
||||
update(db, { id: SessionMessage.ID.make(event.id), type: "session.next.compaction.ended", data })
|
||||
}),
|
||||
]
|
||||
@@ -5,7 +5,8 @@ import { SyncEvent } from "@/sync"
|
||||
import * as Session from "./session"
|
||||
import { MessageV2 } from "./message-v2"
|
||||
import { SessionTable, MessageTable, PartTable } from "./session.sql"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Log } from "@opencode-ai/core/util/log"
|
||||
import nextProjectors from "./projectors-next"
|
||||
|
||||
const log = Log.create({ service: "session.projector" })
|
||||
|
||||
@@ -136,4 +137,6 @@ export default [
|
||||
log.warn("ignored late part update", { partID: id, messageID, sessionID })
|
||||
}
|
||||
}),
|
||||
|
||||
...nextProjectors,
|
||||
]
|
||||
|
||||
@@ -53,6 +53,13 @@ import { InstanceState } from "@/effect/instance-state"
|
||||
import { TaskTool, type TaskPromptOps } from "@/tool/task"
|
||||
import { SessionRunState } from "./run-state"
|
||||
import { EffectBridge } from "@/effect/bridge"
|
||||
import { SessionEvent } from "@/v2/session-event"
|
||||
import { AgentAttachment, FileAttachment, Source } from "@/v2/session-prompt"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import * as DateTime from "effect/DateTime"
|
||||
import { eq } from "@/storage/db"
|
||||
import * as Database from "@/storage/db"
|
||||
import { SessionTable } from "./session.sql"
|
||||
|
||||
// @ts-ignore
|
||||
globalThis.AI_SDK_LOG_WARNINGS = false
|
||||
@@ -924,6 +931,34 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
format: input.format,
|
||||
}
|
||||
|
||||
const current = Database.use((db) =>
|
||||
db
|
||||
.select({ agent: SessionTable.agent, model: SessionTable.model })
|
||||
.from(SessionTable)
|
||||
.where(eq(SessionTable.id, input.sessionID))
|
||||
.get(),
|
||||
)
|
||||
if (current?.agent !== info.agent) {
|
||||
SyncEvent.run(SessionEvent.AgentSwitched.Sync, {
|
||||
sessionID: input.sessionID,
|
||||
timestamp: DateTime.makeUnsafe(info.time.created),
|
||||
agent: info.agent,
|
||||
})
|
||||
}
|
||||
if (
|
||||
current?.model?.providerID !== info.model.providerID ||
|
||||
current.model.id !== info.model.modelID ||
|
||||
current.model.variant !== info.model.variant
|
||||
) {
|
||||
SyncEvent.run(SessionEvent.ModelSwitched.Sync, {
|
||||
sessionID: input.sessionID,
|
||||
timestamp: DateTime.makeUnsafe(info.time.created),
|
||||
id: info.model.modelID,
|
||||
providerID: info.model.providerID,
|
||||
variant: info.model.variant,
|
||||
})
|
||||
}
|
||||
|
||||
yield* Effect.addFinalizer(() => instruction.clear(info.id))
|
||||
|
||||
type Draft<T> = T extends MessageV2.Part ? Omit<T, "id"> & { id?: string } : never
|
||||
@@ -1240,6 +1275,69 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
||||
|
||||
yield* sessions.updateMessage(info)
|
||||
for (const part of parts) yield* sessions.updatePart(part)
|
||||
const nextPrompt = parts.reduce(
|
||||
(result, part) => {
|
||||
if (part.type === "text") {
|
||||
if (part.synthetic) result.synthetic.push(part.text)
|
||||
else result.text.push(part.text)
|
||||
}
|
||||
if (part.type === "file") {
|
||||
result.files.push(
|
||||
new FileAttachment({
|
||||
uri: part.url,
|
||||
mime: part.mime,
|
||||
name: part.filename,
|
||||
source: part.source
|
||||
? new Source({
|
||||
start: part.source.text.start,
|
||||
end: part.source.text.end,
|
||||
text: part.source.text.value,
|
||||
})
|
||||
: undefined,
|
||||
}),
|
||||
)
|
||||
}
|
||||
if (part.type === "agent") {
|
||||
result.agents.push(
|
||||
new AgentAttachment({
|
||||
name: part.name,
|
||||
source: part.source
|
||||
? new Source({
|
||||
start: part.source.start,
|
||||
end: part.source.end,
|
||||
text: part.source.value,
|
||||
})
|
||||
: undefined,
|
||||
}),
|
||||
)
|
||||
}
|
||||
return result
|
||||
},
|
||||
{
|
||||
text: [] as string[],
|
||||
files: [] as FileAttachment[],
|
||||
agents: [] as AgentAttachment[],
|
||||
synthetic: [] as string[],
|
||||
},
|
||||
)
|
||||
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
|
||||
SyncEvent.run(SessionEvent.Prompted.Sync, {
|
||||
sessionID: input.sessionID,
|
||||
timestamp: DateTime.makeUnsafe(info.time.created),
|
||||
prompt: {
|
||||
text: nextPrompt.text.join("\n"),
|
||||
files: nextPrompt.files,
|
||||
agents: nextPrompt.agents,
|
||||
},
|
||||
})
|
||||
for (const text of nextPrompt.synthetic) {
|
||||
// TODO(v2): Temporary dual-write while migrating session messages to v2 events.
|
||||
SyncEvent.run(SessionEvent.Synthetic.Sync, {
|
||||
sessionID: input.sessionID,
|
||||
timestamp: DateTime.makeUnsafe(info.time.created),
|
||||
text,
|
||||
})
|
||||
}
|
||||
|
||||
return { info, parts }
|
||||
}, Effect.scoped)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { sqliteTable, text, integer, index, primaryKey } from "drizzle-orm/sqlite-core"
|
||||
import { ProjectTable } from "../project/project.sql"
|
||||
import type { MessageV2 } from "./message-v2"
|
||||
import type { SessionEntry } from "../v2/session-entry"
|
||||
import type { SessionMessage } from "../v2/session-message"
|
||||
import type { Snapshot } from "../snapshot"
|
||||
import type { Permission } from "../permission"
|
||||
import type { ProjectID } from "../project/schema"
|
||||
@@ -11,6 +11,7 @@ import { Timestamps } from "../storage/schema.sql"
|
||||
|
||||
type PartData = Omit<MessageV2.Part, "id" | "sessionID" | "messageID">
|
||||
type InfoData = Omit<MessageV2.Info, "id" | "sessionID">
|
||||
type SessionMessageData = Omit<(typeof SessionMessage.Message)["Encoded"], "type" | "id">
|
||||
|
||||
export const SessionTable = sqliteTable(
|
||||
"session",
|
||||
@@ -34,6 +35,12 @@ export const SessionTable = sqliteTable(
|
||||
summary_diffs: text({ mode: "json" }).$type<Snapshot.FileDiff[]>(),
|
||||
revert: text({ mode: "json" }).$type<{ messageID: MessageID; partID?: PartID; snapshot?: string; diff?: string }>(),
|
||||
permission: text({ mode: "json" }).$type<Permission.Ruleset>(),
|
||||
agent: text(),
|
||||
model: text({ mode: "json" }).$type<{
|
||||
id: string
|
||||
providerID: string
|
||||
variant?: string
|
||||
}>(),
|
||||
...Timestamps,
|
||||
time_compacting: integer(),
|
||||
time_archived: integer(),
|
||||
@@ -96,22 +103,22 @@ export const TodoTable = sqliteTable(
|
||||
],
|
||||
)
|
||||
|
||||
export const SessionEntryTable = sqliteTable(
|
||||
"session_entry",
|
||||
export const SessionMessageTable = sqliteTable(
|
||||
"session_message",
|
||||
{
|
||||
id: text().$type<SessionEntry.ID>().primaryKey(),
|
||||
id: text().$type<SessionMessage.ID>().primaryKey(),
|
||||
session_id: text()
|
||||
.$type<SessionID>()
|
||||
.notNull()
|
||||
.references(() => SessionTable.id, { onDelete: "cascade" }),
|
||||
type: text().$type<SessionEntry.Type>().notNull(),
|
||||
type: text().$type<SessionMessage.Type>().notNull(),
|
||||
...Timestamps,
|
||||
data: text({ mode: "json" }).notNull().$type<Omit<SessionEntry.Entry, "type" | "id">>(),
|
||||
data: text({ mode: "json" }).notNull().$type<SessionMessageData>(),
|
||||
},
|
||||
(table) => [
|
||||
index("session_entry_session_idx").on(table.session_id),
|
||||
index("session_entry_session_type_idx").on(table.session_id, table.type),
|
||||
index("session_entry_time_created_idx").on(table.time_created),
|
||||
index("session_message_session_idx").on(table.session_id),
|
||||
index("session_message_session_type_idx").on(table.session_id, table.type),
|
||||
index("session_message_time_created_idx").on(table.time_created),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import { Snapshot } from "@/snapshot"
|
||||
import { ProjectID } from "../project/schema"
|
||||
import { WorkspaceID } from "../control-plane/schema"
|
||||
import { SessionID, MessageID, PartID } from "./schema"
|
||||
import { ModelID, ProviderID } from "@/provider/schema"
|
||||
|
||||
import type { Provider } from "@/provider/provider"
|
||||
import { Permission } from "@/permission"
|
||||
@@ -78,6 +79,10 @@ export function fromRow(row: SessionRow): Info {
|
||||
path: row.path ?? undefined,
|
||||
parentID: row.parent_id ?? undefined,
|
||||
title: row.title,
|
||||
agent: row.agent ?? undefined,
|
||||
model: row.model
|
||||
? { id: ModelID.make(row.model.id), providerID: ProviderID.make(row.model.providerID), variant: row.model.variant }
|
||||
: undefined,
|
||||
version: row.version,
|
||||
summary,
|
||||
share,
|
||||
@@ -102,6 +107,8 @@ export function toRow(info: Info) {
|
||||
directory: info.directory,
|
||||
path: info.path,
|
||||
title: info.title,
|
||||
agent: info.agent,
|
||||
model: info.model,
|
||||
version: info.version,
|
||||
share_url: info.share?.url,
|
||||
summary_additions: info.summary?.additions,
|
||||
@@ -160,6 +167,12 @@ const Revert = Schema.Struct({
|
||||
diff: optionalOmitUndefined(Schema.String),
|
||||
})
|
||||
|
||||
const Model = Schema.Struct({
|
||||
id: ModelID,
|
||||
providerID: ProviderID,
|
||||
variant: optionalOmitUndefined(Schema.String),
|
||||
})
|
||||
|
||||
export const Info = Schema.Struct({
|
||||
id: SessionID,
|
||||
slug: Schema.String,
|
||||
@@ -171,6 +184,8 @@ export const Info = Schema.Struct({
|
||||
summary: optionalOmitUndefined(Summary),
|
||||
share: optionalOmitUndefined(Share),
|
||||
title: Schema.String,
|
||||
agent: optionalOmitUndefined(Schema.String),
|
||||
model: optionalOmitUndefined(Model),
|
||||
version: Schema.String,
|
||||
time: Time,
|
||||
permission: optionalOmitUndefined(Permission.Ruleset),
|
||||
@@ -201,6 +216,8 @@ export const CreateInput = Schema.optional(
|
||||
Schema.Struct({
|
||||
parentID: Schema.optional(SessionID),
|
||||
title: Schema.optional(Schema.String),
|
||||
agent: Schema.optional(Schema.String),
|
||||
model: Schema.optional(Model),
|
||||
permission: Schema.optional(Permission.Ruleset),
|
||||
workspaceID: Schema.optional(WorkspaceID),
|
||||
}),
|
||||
@@ -272,6 +289,8 @@ const UpdatedInfo = Schema.Struct({
|
||||
summary: Schema.optional(Schema.NullOr(Summary)),
|
||||
share: Schema.optional(UpdatedShare),
|
||||
title: Schema.optional(Schema.NullOr(Schema.String)),
|
||||
agent: Schema.optional(Schema.NullOr(Schema.String)),
|
||||
model: Schema.optional(Schema.NullOr(Model)),
|
||||
version: Schema.optional(Schema.NullOr(Schema.String)),
|
||||
time: Schema.optional(UpdatedTime),
|
||||
permission: Schema.optional(Schema.NullOr(Permission.Ruleset)),
|
||||
@@ -404,6 +423,8 @@ export interface Interface {
|
||||
readonly create: (input?: {
|
||||
parentID?: SessionID
|
||||
title?: string
|
||||
agent?: string
|
||||
model?: Schema.Schema.Type<typeof Model>
|
||||
permission?: Permission.Ruleset
|
||||
workspaceID?: WorkspaceID
|
||||
}) => Effect.Effect<Info>
|
||||
@@ -464,6 +485,8 @@ export const layer: Layer.Layer<Service, never, Bus.Service | Storage.Service |
|
||||
const createNext = Effect.fn("Session.createNext")(function* (input: {
|
||||
id?: SessionID
|
||||
title?: string
|
||||
agent?: string
|
||||
model?: Schema.Schema.Type<typeof Model>
|
||||
parentID?: SessionID
|
||||
workspaceID?: WorkspaceID
|
||||
directory: string
|
||||
@@ -481,6 +504,8 @@ export const layer: Layer.Layer<Service, never, Bus.Service | Storage.Service |
|
||||
workspaceID: input.workspaceID,
|
||||
parentID: input.parentID,
|
||||
title: input.title ?? createDefaultTitle(!!input.parentID),
|
||||
agent: input.agent,
|
||||
model: input.model,
|
||||
permission: input.permission,
|
||||
time: {
|
||||
created: Date.now(),
|
||||
@@ -591,6 +616,8 @@ export const layer: Layer.Layer<Service, never, Bus.Service | Storage.Service |
|
||||
const create = Effect.fn("Session.create")(function* (input?: {
|
||||
parentID?: SessionID
|
||||
title?: string
|
||||
agent?: string
|
||||
model?: Schema.Schema.Type<typeof Model>
|
||||
permission?: Permission.Ruleset
|
||||
workspaceID?: WorkspaceID
|
||||
}) {
|
||||
@@ -601,6 +628,8 @@ export const layer: Layer.Layer<Service, never, Bus.Service | Storage.Service |
|
||||
directory: ctx.directory,
|
||||
path: sessionPath(ctx.worktree, ctx.directory),
|
||||
title: input?.title,
|
||||
agent: input?.agent,
|
||||
model: input?.model,
|
||||
permission: input?.permission,
|
||||
workspaceID: input?.workspaceID ?? workspace,
|
||||
})
|
||||
|
||||
@@ -46,7 +46,7 @@ export type Properties<Def extends Definition = Definition> = EffectSchema.Schem
|
||||
|
||||
export type SerializedEvent<Def extends Definition = Definition> = Event<Def> & { type: string }
|
||||
|
||||
type ProjectorFunc = (db: Database.TxOrDb, data: unknown) => void
|
||||
type ProjectorFunc = (db: Database.TxOrDb, data: unknown, event: Event) => void
|
||||
type ConvertEvent = (type: string, data: Event["data"]) => unknown | Promise<unknown>
|
||||
type PublishContext = {
|
||||
instance?: InstanceContext
|
||||
@@ -255,7 +255,7 @@ export function define<
|
||||
|
||||
export function project<Def extends Definition>(
|
||||
def: Def,
|
||||
func: (db: Database.TxOrDb, data: Event<Def>["data"]) => void,
|
||||
func: (db: Database.TxOrDb, data: Event<Def>["data"], event: Event<Def>) => void,
|
||||
): [Definition, ProjectorFunc] {
|
||||
return [def, func as ProjectorFunc]
|
||||
}
|
||||
@@ -277,7 +277,7 @@ function process<Def extends Definition>(
|
||||
// idempotent: need to ignore any events already logged
|
||||
|
||||
Database.transaction((tx) => {
|
||||
projector(tx, event.data)
|
||||
projector(tx, event.data, event)
|
||||
|
||||
if (Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) {
|
||||
tx.insert(EventSequenceTable)
|
||||
@@ -308,7 +308,7 @@ function process<Def extends Definition>(
|
||||
}
|
||||
|
||||
const result = convertEvent(def.type, event.data)
|
||||
const publish = (data: unknown) => ProjectBus.publish(def, data as Properties<Def>)
|
||||
const publish = (data: unknown) => ProjectBus.publish(def, data as Properties<Def>, { id: event.id })
|
||||
if (result instanceof Promise) {
|
||||
void result.then(publish)
|
||||
} else {
|
||||
|
||||
@@ -90,7 +90,7 @@ function bodyWithChecks(ast: SchemaAST.AST): z.ZodTypeAny {
|
||||
// Schema.withDecodingDefault also attaches encoding, but we want `.default(v)`
|
||||
// on the inner Zod rather than a transform wrapper — so optional ASTs whose
|
||||
// encoding resolves a default from Option.none() route through body()/opt().
|
||||
const hasEncoding = ast.encoding?.length && ast._tag !== "Declaration"
|
||||
const hasEncoding = ast.encoding?.length && (ast._tag !== "Declaration" || ast.typeParameters.length === 0)
|
||||
const hasTransform = hasEncoding && !(SchemaAST.isOptional(ast) && extractDefault(ast) !== undefined)
|
||||
const base = hasTransform ? encoded(ast) : body(ast)
|
||||
return ast.checks?.length ? applyChecks(base, ast.checks, ast) : base
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Identifier } from "@/id/id"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import * as Schema from "effect/Schema"
|
||||
|
||||
export const ID = Schema.String.pipe(
|
||||
Schema.brand("Event.ID"),
|
||||
withStatics((s) => ({
|
||||
create: () => s.make(Identifier.create("evt", "ascending")),
|
||||
})),
|
||||
)
|
||||
export type ID = Schema.Schema.Type<typeof ID>
|
||||
|
||||
export function define<const Type extends string, Fields extends Schema.Struct.Fields>(input: {
|
||||
type: Type
|
||||
schema: Fields
|
||||
aggregate: string
|
||||
version?: number
|
||||
}) {
|
||||
const Payload = Schema.Struct({
|
||||
id: ID,
|
||||
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
type: Schema.Literal(input.type),
|
||||
data: Schema.Struct(input.schema),
|
||||
}).annotate({
|
||||
identifier: input.type,
|
||||
})
|
||||
|
||||
const Sync = SyncEvent.define({
|
||||
type: input.type,
|
||||
version: input.version ?? 1,
|
||||
aggregate: input.aggregate,
|
||||
schema: Payload.fields.data,
|
||||
})
|
||||
|
||||
return Object.assign(Payload, {
|
||||
Sync,
|
||||
version: input.version,
|
||||
aggregate: input.aggregate,
|
||||
})
|
||||
}
|
||||
|
||||
export * as Event from "./event"
|
||||
@@ -1,261 +0,0 @@
|
||||
import { produce, type WritableDraft } from "immer"
|
||||
import { SessionEvent } from "./session-event"
|
||||
import { SessionEntry } from "./session-entry"
|
||||
|
||||
export type MemoryState = {
|
||||
entries: SessionEntry.Entry[]
|
||||
pending: SessionEntry.Entry[]
|
||||
}
|
||||
|
||||
export interface Adapter<Result> {
|
||||
readonly getCurrentAssistant: () => SessionEntry.Assistant | undefined
|
||||
readonly updateAssistant: (assistant: SessionEntry.Assistant) => void
|
||||
readonly appendEntry: (entry: SessionEntry.Entry) => void
|
||||
readonly appendPending: (entry: SessionEntry.Entry) => void
|
||||
readonly finish: () => Result
|
||||
}
|
||||
|
||||
export function memory(state: MemoryState): Adapter<MemoryState> {
|
||||
const activeAssistantIndex = () =>
|
||||
state.entries.findLastIndex((entry) => entry.type === "assistant" && !entry.time.completed)
|
||||
|
||||
return {
|
||||
getCurrentAssistant() {
|
||||
const index = activeAssistantIndex()
|
||||
if (index < 0) return
|
||||
const assistant = state.entries[index]
|
||||
return assistant?.type === "assistant" ? assistant : undefined
|
||||
},
|
||||
updateAssistant(assistant) {
|
||||
const index = activeAssistantIndex()
|
||||
if (index < 0) return
|
||||
const current = state.entries[index]
|
||||
if (current?.type !== "assistant") return
|
||||
state.entries[index] = assistant
|
||||
},
|
||||
appendEntry(entry) {
|
||||
state.entries.push(entry)
|
||||
},
|
||||
appendPending(entry) {
|
||||
state.pending.push(entry)
|
||||
},
|
||||
finish() {
|
||||
return state
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function stepWith<Result>(adapter: Adapter<Result>, event: SessionEvent.Event): Result {
|
||||
const currentAssistant = adapter.getCurrentAssistant()
|
||||
type DraftAssistant = WritableDraft<SessionEntry.Assistant>
|
||||
type DraftTool = WritableDraft<SessionEntry.AssistantTool>
|
||||
type DraftText = WritableDraft<SessionEntry.AssistantText>
|
||||
type DraftReasoning = WritableDraft<SessionEntry.AssistantReasoning>
|
||||
|
||||
const latestTool = (assistant: DraftAssistant | undefined, callID?: string) =>
|
||||
assistant?.content.findLast(
|
||||
(item): item is DraftTool => item.type === "tool" && (callID === undefined || item.callID === callID),
|
||||
)
|
||||
|
||||
const latestText = (assistant: DraftAssistant | undefined) =>
|
||||
assistant?.content.findLast((item): item is DraftText => item.type === "text")
|
||||
|
||||
const latestReasoning = (assistant: DraftAssistant | undefined) =>
|
||||
assistant?.content.findLast((item): item is DraftReasoning => item.type === "reasoning")
|
||||
|
||||
SessionEvent.Event.match(event, {
|
||||
prompt: (event) => {
|
||||
const entry = SessionEntry.User.fromEvent(event)
|
||||
if (currentAssistant) {
|
||||
adapter.appendPending(entry)
|
||||
return
|
||||
}
|
||||
adapter.appendEntry(entry)
|
||||
},
|
||||
synthetic: (event) => {
|
||||
adapter.appendEntry(SessionEntry.Synthetic.fromEvent(event))
|
||||
},
|
||||
"step.started": (event) => {
|
||||
if (currentAssistant) {
|
||||
adapter.updateAssistant(
|
||||
produce(currentAssistant, (draft) => {
|
||||
draft.time.completed = event.timestamp
|
||||
}),
|
||||
)
|
||||
}
|
||||
adapter.appendEntry(SessionEntry.Assistant.fromEvent(event))
|
||||
},
|
||||
"step.ended": (event) => {
|
||||
if (currentAssistant) {
|
||||
adapter.updateAssistant(
|
||||
produce(currentAssistant, (draft) => {
|
||||
draft.time.completed = event.timestamp
|
||||
draft.cost = event.cost
|
||||
draft.tokens = event.tokens
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
"text.started": () => {
|
||||
if (currentAssistant) {
|
||||
adapter.updateAssistant(
|
||||
produce(currentAssistant, (draft) => {
|
||||
draft.content.push({
|
||||
type: "text",
|
||||
text: "",
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
"text.delta": (event) => {
|
||||
if (currentAssistant) {
|
||||
adapter.updateAssistant(
|
||||
produce(currentAssistant, (draft) => {
|
||||
const match = latestText(draft)
|
||||
if (match) match.text += event.delta
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
"text.ended": () => {},
|
||||
"tool.input.started": (event) => {
|
||||
if (currentAssistant) {
|
||||
adapter.updateAssistant(
|
||||
produce(currentAssistant, (draft) => {
|
||||
draft.content.push({
|
||||
type: "tool",
|
||||
callID: event.callID,
|
||||
name: event.name,
|
||||
time: {
|
||||
created: event.timestamp,
|
||||
},
|
||||
state: {
|
||||
status: "pending",
|
||||
input: "",
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
"tool.input.delta": (event) => {
|
||||
if (currentAssistant) {
|
||||
adapter.updateAssistant(
|
||||
produce(currentAssistant, (draft) => {
|
||||
const match = latestTool(draft, event.callID)
|
||||
// oxlint-disable-next-line no-base-to-string -- event.delta is a Schema.String (runtime string)
|
||||
if (match && match.state.status === "pending") match.state.input += event.delta
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
"tool.input.ended": () => {},
|
||||
"tool.called": (event) => {
|
||||
if (currentAssistant) {
|
||||
adapter.updateAssistant(
|
||||
produce(currentAssistant, (draft) => {
|
||||
const match = latestTool(draft, event.callID)
|
||||
if (match) {
|
||||
match.time.ran = event.timestamp
|
||||
match.state = {
|
||||
status: "running",
|
||||
input: event.input,
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
"tool.success": (event) => {
|
||||
if (currentAssistant) {
|
||||
adapter.updateAssistant(
|
||||
produce(currentAssistant, (draft) => {
|
||||
const match = latestTool(draft, event.callID)
|
||||
if (match && match.state.status === "running") {
|
||||
match.state = {
|
||||
status: "completed",
|
||||
input: match.state.input,
|
||||
output: event.output ?? "",
|
||||
title: event.title,
|
||||
metadata: event.metadata ?? {},
|
||||
attachments: [...(event.attachments ?? [])],
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
"tool.error": (event) => {
|
||||
if (currentAssistant) {
|
||||
adapter.updateAssistant(
|
||||
produce(currentAssistant, (draft) => {
|
||||
const match = latestTool(draft, event.callID)
|
||||
if (match && match.state.status === "running") {
|
||||
match.state = {
|
||||
status: "error",
|
||||
error: event.error,
|
||||
input: match.state.input,
|
||||
metadata: event.metadata ?? {},
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
"reasoning.started": () => {
|
||||
if (currentAssistant) {
|
||||
adapter.updateAssistant(
|
||||
produce(currentAssistant, (draft) => {
|
||||
draft.content.push({
|
||||
type: "reasoning",
|
||||
text: "",
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
"reasoning.delta": (event) => {
|
||||
if (currentAssistant) {
|
||||
adapter.updateAssistant(
|
||||
produce(currentAssistant, (draft) => {
|
||||
const match = latestReasoning(draft)
|
||||
if (match) match.text += event.delta
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
"reasoning.ended": (event) => {
|
||||
if (currentAssistant) {
|
||||
adapter.updateAssistant(
|
||||
produce(currentAssistant, (draft) => {
|
||||
const match = latestReasoning(draft)
|
||||
if (match) match.text = event.text
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
retried: (event) => {
|
||||
if (currentAssistant) {
|
||||
adapter.updateAssistant(
|
||||
produce(currentAssistant, (draft) => {
|
||||
draft.retries = [...(draft.retries ?? []), SessionEntry.AssistantRetry.fromEvent(event)]
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
compacted: (event) => {
|
||||
adapter.appendEntry(SessionEntry.Compaction.fromEvent(event))
|
||||
},
|
||||
})
|
||||
|
||||
return adapter.finish()
|
||||
}
|
||||
|
||||
export function step(old: MemoryState, event: SessionEvent.Event): MemoryState {
|
||||
return produce(old, (draft) => {
|
||||
stepWith(memory(draft as MemoryState), event)
|
||||
})
|
||||
}
|
||||
|
||||
export * as SessionEntryStepper from "./session-entry-stepper"
|
||||
@@ -1,220 +0,0 @@
|
||||
import { Schema } from "effect"
|
||||
import { NonNegativeInt } from "@/util/schema"
|
||||
import { SessionEvent } from "./session-event"
|
||||
|
||||
export const ID = SessionEvent.ID
|
||||
export type ID = Schema.Schema.Type<typeof ID>
|
||||
|
||||
const Base = {
|
||||
id: SessionEvent.ID,
|
||||
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
time: Schema.Struct({
|
||||
created: Schema.DateTimeUtc,
|
||||
}),
|
||||
}
|
||||
|
||||
export class User extends Schema.Class<User>("Session.Entry.User")({
|
||||
...Base,
|
||||
text: SessionEvent.Prompt.fields.text,
|
||||
files: SessionEvent.Prompt.fields.files,
|
||||
agents: SessionEvent.Prompt.fields.agents,
|
||||
type: Schema.Literal("user"),
|
||||
time: Schema.Struct({
|
||||
created: Schema.DateTimeUtc,
|
||||
}),
|
||||
}) {
|
||||
static fromEvent(event: SessionEvent.Prompt) {
|
||||
return new User({
|
||||
id: event.id,
|
||||
type: "user",
|
||||
metadata: event.metadata,
|
||||
text: event.text,
|
||||
files: event.files,
|
||||
agents: event.agents,
|
||||
time: { created: event.timestamp },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class Synthetic extends Schema.Class<Synthetic>("Session.Entry.Synthetic")({
|
||||
...SessionEvent.Synthetic.fields,
|
||||
...Base,
|
||||
type: Schema.Literal("synthetic"),
|
||||
}) {
|
||||
static fromEvent(event: SessionEvent.Synthetic) {
|
||||
return new Synthetic({
|
||||
...event,
|
||||
time: { created: event.timestamp },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class ToolStatePending extends Schema.Class<ToolStatePending>("Session.Entry.ToolState.Pending")({
|
||||
status: Schema.Literal("pending"),
|
||||
input: Schema.String,
|
||||
}) {}
|
||||
|
||||
export class ToolStateRunning extends Schema.Class<ToolStateRunning>("Session.Entry.ToolState.Running")({
|
||||
status: Schema.Literal("running"),
|
||||
input: Schema.Record(Schema.String, Schema.Unknown),
|
||||
title: Schema.String.pipe(Schema.optional),
|
||||
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class ToolStateCompleted extends Schema.Class<ToolStateCompleted>("Session.Entry.ToolState.Completed")({
|
||||
status: Schema.Literal("completed"),
|
||||
input: Schema.Record(Schema.String, Schema.Unknown),
|
||||
output: Schema.String,
|
||||
title: Schema.String,
|
||||
metadata: Schema.Record(Schema.String, Schema.Unknown),
|
||||
attachments: SessionEvent.FileAttachment.pipe(Schema.Array, Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class ToolStateError extends Schema.Class<ToolStateError>("Session.Entry.ToolState.Error")({
|
||||
status: Schema.Literal("error"),
|
||||
input: Schema.Record(Schema.String, Schema.Unknown),
|
||||
error: Schema.String,
|
||||
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export const ToolState = Schema.Union([ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]).pipe(
|
||||
Schema.toTaggedUnion("status"),
|
||||
)
|
||||
export type ToolState = Schema.Schema.Type<typeof ToolState>
|
||||
|
||||
export class AssistantTool extends Schema.Class<AssistantTool>("Session.Entry.Assistant.Tool")({
|
||||
type: Schema.Literal("tool"),
|
||||
callID: Schema.String,
|
||||
name: Schema.String,
|
||||
state: ToolState,
|
||||
time: Schema.Struct({
|
||||
created: Schema.DateTimeUtc,
|
||||
ran: Schema.DateTimeUtc.pipe(Schema.optional),
|
||||
completed: Schema.DateTimeUtc.pipe(Schema.optional),
|
||||
pruned: Schema.DateTimeUtc.pipe(Schema.optional),
|
||||
}),
|
||||
}) {}
|
||||
|
||||
export class AssistantText extends Schema.Class<AssistantText>("Session.Entry.Assistant.Text")({
|
||||
type: Schema.Literal("text"),
|
||||
text: Schema.String,
|
||||
}) {}
|
||||
|
||||
export class AssistantReasoning extends Schema.Class<AssistantReasoning>("Session.Entry.Assistant.Reasoning")({
|
||||
type: Schema.Literal("reasoning"),
|
||||
text: Schema.String,
|
||||
}) {}
|
||||
|
||||
export class AssistantRetry extends Schema.Class<AssistantRetry>("Session.Entry.Assistant.Retry")({
|
||||
attempt: NonNegativeInt,
|
||||
error: SessionEvent.RetryError,
|
||||
time: Schema.Struct({
|
||||
created: Schema.DateTimeUtc,
|
||||
}),
|
||||
}) {
|
||||
static fromEvent(event: SessionEvent.Retried) {
|
||||
return new AssistantRetry({
|
||||
attempt: event.attempt,
|
||||
error: event.error,
|
||||
time: {
|
||||
created: event.timestamp,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const AssistantContent = Schema.Union([AssistantText, AssistantReasoning, AssistantTool]).pipe(
|
||||
Schema.toTaggedUnion("type"),
|
||||
)
|
||||
export type AssistantContent = Schema.Schema.Type<typeof AssistantContent>
|
||||
|
||||
export class Assistant extends Schema.Class<Assistant>("Session.Entry.Assistant")({
|
||||
...Base,
|
||||
type: Schema.Literal("assistant"),
|
||||
content: AssistantContent.pipe(Schema.Array),
|
||||
retries: AssistantRetry.pipe(Schema.Array, Schema.optional),
|
||||
cost: Schema.Finite.pipe(Schema.optional),
|
||||
tokens: Schema.Struct({
|
||||
input: NonNegativeInt,
|
||||
output: NonNegativeInt,
|
||||
reasoning: NonNegativeInt,
|
||||
cache: Schema.Struct({
|
||||
read: NonNegativeInt,
|
||||
write: NonNegativeInt,
|
||||
}),
|
||||
}).pipe(Schema.optional),
|
||||
error: Schema.String.pipe(Schema.optional),
|
||||
time: Schema.Struct({
|
||||
created: Schema.DateTimeUtc,
|
||||
completed: Schema.DateTimeUtc.pipe(Schema.optional),
|
||||
}),
|
||||
}) {
|
||||
static fromEvent(event: SessionEvent.Step.Started) {
|
||||
return new Assistant({
|
||||
id: event.id,
|
||||
type: "assistant",
|
||||
time: {
|
||||
created: event.timestamp,
|
||||
},
|
||||
content: [],
|
||||
retries: [],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class Compaction extends Schema.Class<Compaction>("Session.Entry.Compaction")({
|
||||
...SessionEvent.Compacted.fields,
|
||||
type: Schema.Literal("compaction"),
|
||||
...Base,
|
||||
}) {
|
||||
static fromEvent(event: SessionEvent.Compacted) {
|
||||
return new Compaction({
|
||||
...event,
|
||||
type: "compaction",
|
||||
time: { created: event.timestamp },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const Entry = Schema.Union([User, Synthetic, Assistant, Compaction]).pipe(Schema.toTaggedUnion("type"))
|
||||
|
||||
export type Entry = Schema.Schema.Type<typeof Entry>
|
||||
|
||||
export type Type = Entry["type"]
|
||||
|
||||
/*
|
||||
export interface Interface {
|
||||
readonly decode: (row: typeof SessionEntryTable.$inferSelect) => Entry
|
||||
readonly fromSession: (sessionID: SessionID) => Effect.Effect<Entry[], never>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionEntry") {}
|
||||
|
||||
export const layer: Layer.Layer<Service, never, never> = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const decodeEntry = Schema.decodeUnknownSync(Entry)
|
||||
|
||||
const decode: (typeof Service.Service)["decode"] = (row) => decodeEntry({ ...row, id: row.id, type: row.type })
|
||||
|
||||
const fromSession = Effect.fn("SessionEntry.fromSession")(function* (sessionID: SessionID) {
|
||||
return Database.use((db) =>
|
||||
db
|
||||
.select()
|
||||
.from(SessionEntryTable)
|
||||
.where(eq(SessionEntryTable.session_id, sessionID))
|
||||
.orderBy(SessionEntryTable.id)
|
||||
.all()
|
||||
.map((row) => decode(row)),
|
||||
)
|
||||
})
|
||||
|
||||
return Service.of({
|
||||
decode,
|
||||
fromSession,
|
||||
})
|
||||
}),
|
||||
)
|
||||
*/
|
||||
|
||||
export * as SessionEntry from "./session-entry"
|
||||
@@ -1,128 +1,94 @@
|
||||
import { Identifier } from "@/id/id"
|
||||
import { NonNegativeInt, withStatics } from "@/util/schema"
|
||||
import * as DateTime from "effect/DateTime"
|
||||
import { SessionID } from "@/session/schema"
|
||||
import { NonNegativeInt } from "@/util/schema"
|
||||
import { Event } from "./event"
|
||||
import { FileAttachment, Prompt } from "./session-prompt"
|
||||
import { Schema } from "effect"
|
||||
export { FileAttachment }
|
||||
import { ToolOutput } from "./tool-output"
|
||||
import { ModelID, ProviderID } from "@/provider/schema"
|
||||
|
||||
export namespace SessionEvent {
|
||||
export const ID = Schema.String.pipe(
|
||||
Schema.brand("Session.Event.ID"),
|
||||
withStatics((s) => ({
|
||||
create: () => s.make(Identifier.create("evt", "ascending")),
|
||||
})),
|
||||
)
|
||||
export type ID = Schema.Schema.Type<typeof ID>
|
||||
type Stamp = Schema.Schema.Type<typeof Schema.DateTimeUtc>
|
||||
type BaseInput = {
|
||||
id?: ID
|
||||
metadata?: Record<string, unknown>
|
||||
timestamp?: Stamp
|
||||
}
|
||||
export const Source = Schema.Struct({
|
||||
start: NonNegativeInt,
|
||||
end: NonNegativeInt,
|
||||
text: Schema.String,
|
||||
}).annotate({
|
||||
identifier: "session.next.event.source",
|
||||
})
|
||||
export type Source = Schema.Schema.Type<typeof Source>
|
||||
|
||||
const Base = {
|
||||
id: ID,
|
||||
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
timestamp: Schema.DateTimeUtc,
|
||||
}
|
||||
const Base = {
|
||||
timestamp: Schema.DateTimeUtcFromMillis,
|
||||
sessionID: SessionID,
|
||||
}
|
||||
|
||||
export class Source extends Schema.Class<Source>("Session.Event.Source")({
|
||||
start: NonNegativeInt,
|
||||
end: NonNegativeInt,
|
||||
text: Schema.String,
|
||||
}) {}
|
||||
|
||||
export class FileAttachment extends Schema.Class<FileAttachment>("Session.Event.FileAttachment")({
|
||||
uri: Schema.String,
|
||||
mime: Schema.String,
|
||||
name: Schema.String.pipe(Schema.optional),
|
||||
description: Schema.String.pipe(Schema.optional),
|
||||
source: Source.pipe(Schema.optional),
|
||||
}) {
|
||||
static create(input: FileAttachment) {
|
||||
return new FileAttachment({
|
||||
uri: input.uri,
|
||||
mime: input.mime,
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
source: input.source,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class AgentAttachment extends Schema.Class<AgentAttachment>("Session.Event.AgentAttachment")({
|
||||
name: Schema.String,
|
||||
source: Source.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class RetryError extends Schema.Class<RetryError>("Session.Event.Retry.Error")({
|
||||
message: Schema.String,
|
||||
statusCode: NonNegativeInt.pipe(Schema.optional),
|
||||
isRetryable: Schema.Boolean,
|
||||
responseHeaders: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
|
||||
responseBody: Schema.String.pipe(Schema.optional),
|
||||
metadata: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class Prompt extends Schema.Class<Prompt>("Session.Event.Prompt")({
|
||||
export const AgentSwitched = Event.define({
|
||||
type: "session.next.agent.switched",
|
||||
aggregate: "sessionID",
|
||||
version: 1,
|
||||
schema: {
|
||||
...Base,
|
||||
type: Schema.Literal("prompt"),
|
||||
text: Schema.String,
|
||||
files: Schema.Array(FileAttachment).pipe(Schema.optional),
|
||||
agents: Schema.Array(AgentAttachment).pipe(Schema.optional),
|
||||
}) {
|
||||
static create(input: BaseInput & { text: string; files?: FileAttachment[]; agents?: AgentAttachment[] }) {
|
||||
return new Prompt({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "prompt",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
text: input.text,
|
||||
files: input.files,
|
||||
agents: input.agents,
|
||||
})
|
||||
}
|
||||
}
|
||||
agent: Schema.String,
|
||||
},
|
||||
})
|
||||
export type AgentSwitched = Schema.Schema.Type<typeof AgentSwitched>
|
||||
|
||||
export class Synthetic extends Schema.Class<Synthetic>("Session.Event.Synthetic")({
|
||||
export const ModelSwitched = Event.define({
|
||||
type: "session.next.model.switched",
|
||||
aggregate: "sessionID",
|
||||
version: 1,
|
||||
schema: {
|
||||
...Base,
|
||||
type: Schema.Literal("synthetic"),
|
||||
text: Schema.String,
|
||||
}) {
|
||||
static create(input: BaseInput & { text: string }) {
|
||||
return new Synthetic({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "synthetic",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
text: input.text,
|
||||
})
|
||||
}
|
||||
}
|
||||
id: ModelID,
|
||||
providerID: ProviderID,
|
||||
variant: Schema.String.pipe(Schema.optional),
|
||||
},
|
||||
})
|
||||
export type ModelSwitched = Schema.Schema.Type<typeof ModelSwitched>
|
||||
|
||||
export namespace Step {
|
||||
export class Started extends Schema.Class<Started>("Session.Event.Step.Started")({
|
||||
export const Prompted = Event.define({
|
||||
type: "session.next.prompted",
|
||||
aggregate: "sessionID",
|
||||
version: 1,
|
||||
schema: {
|
||||
...Base,
|
||||
prompt: Prompt,
|
||||
},
|
||||
})
|
||||
export type Prompted = Schema.Schema.Type<typeof Prompted>
|
||||
|
||||
export const Synthetic = Event.define({
|
||||
type: "session.next.synthetic",
|
||||
aggregate: "sessionID",
|
||||
schema: {
|
||||
...Base,
|
||||
text: Schema.String,
|
||||
},
|
||||
})
|
||||
export type Synthetic = Schema.Schema.Type<typeof Synthetic>
|
||||
|
||||
export namespace Step {
|
||||
export const Started = Event.define({
|
||||
type: "session.next.step.started",
|
||||
aggregate: "sessionID",
|
||||
schema: {
|
||||
...Base,
|
||||
type: Schema.Literal("step.started"),
|
||||
agent: Schema.String,
|
||||
model: Schema.Struct({
|
||||
id: Schema.String,
|
||||
providerID: Schema.String,
|
||||
variant: Schema.String.pipe(Schema.optional),
|
||||
}),
|
||||
}) {
|
||||
static create(input: BaseInput & { model: { id: string; providerID: string; variant?: string } }) {
|
||||
return new Started({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "step.started",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
model: input.model,
|
||||
})
|
||||
}
|
||||
}
|
||||
snapshot: Schema.String.pipe(Schema.optional),
|
||||
},
|
||||
})
|
||||
export type Started = Schema.Schema.Type<typeof Started>
|
||||
|
||||
export class Ended extends Schema.Class<Ended>("Session.Event.Step.Ended")({
|
||||
export const Ended = Event.define({
|
||||
type: "session.next.step.ended",
|
||||
aggregate: "sessionID",
|
||||
schema: {
|
||||
...Base,
|
||||
type: Schema.Literal("step.ended"),
|
||||
reason: Schema.String,
|
||||
finish: Schema.String,
|
||||
cost: Schema.Finite,
|
||||
tokens: Schema.Struct({
|
||||
input: NonNegativeInt,
|
||||
@@ -133,177 +99,118 @@ export namespace SessionEvent {
|
||||
write: NonNegativeInt,
|
||||
}),
|
||||
}),
|
||||
}) {
|
||||
static create(input: BaseInput & { reason: string; cost: number; tokens: Ended["tokens"] }) {
|
||||
return new Ended({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "step.ended",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
reason: input.reason,
|
||||
cost: input.cost,
|
||||
tokens: input.tokens,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
snapshot: Schema.String.pipe(Schema.optional),
|
||||
},
|
||||
})
|
||||
export type Ended = Schema.Schema.Type<typeof Ended>
|
||||
}
|
||||
|
||||
export namespace Text {
|
||||
export class Started extends Schema.Class<Started>("Session.Event.Text.Started")({
|
||||
export namespace Text {
|
||||
export const Started = Event.define({
|
||||
type: "session.next.text.started",
|
||||
aggregate: "sessionID",
|
||||
schema: {
|
||||
...Base,
|
||||
type: Schema.Literal("text.started"),
|
||||
}) {
|
||||
static create(input: BaseInput = {}) {
|
||||
return new Started({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "text.started",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
export type Started = Schema.Schema.Type<typeof Started>
|
||||
|
||||
export class Delta extends Schema.Class<Delta>("Session.Event.Text.Delta")({
|
||||
export const Delta = Event.define({
|
||||
type: "session.next.text.delta",
|
||||
aggregate: "sessionID",
|
||||
schema: {
|
||||
...Base,
|
||||
type: Schema.Literal("text.delta"),
|
||||
delta: Schema.String,
|
||||
}) {
|
||||
static create(input: BaseInput & { delta: string }) {
|
||||
return new Delta({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "text.delta",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
delta: input.delta,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
export type Delta = Schema.Schema.Type<typeof Delta>
|
||||
|
||||
export class Ended extends Schema.Class<Ended>("Session.Event.Text.Ended")({
|
||||
export const Ended = Event.define({
|
||||
type: "session.next.text.ended",
|
||||
aggregate: "sessionID",
|
||||
schema: {
|
||||
...Base,
|
||||
type: Schema.Literal("text.ended"),
|
||||
text: Schema.String,
|
||||
}) {
|
||||
static create(input: BaseInput & { text: string }) {
|
||||
return new Ended({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "text.ended",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
text: input.text,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
export type Ended = Schema.Schema.Type<typeof Ended>
|
||||
}
|
||||
|
||||
export namespace Reasoning {
|
||||
export class Started extends Schema.Class<Started>("Session.Event.Reasoning.Started")({
|
||||
export namespace Reasoning {
|
||||
export const Started = Event.define({
|
||||
type: "session.next.reasoning.started",
|
||||
aggregate: "sessionID",
|
||||
schema: {
|
||||
...Base,
|
||||
type: Schema.Literal("reasoning.started"),
|
||||
}) {
|
||||
static create(input: BaseInput = {}) {
|
||||
return new Started({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "reasoning.started",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
})
|
||||
}
|
||||
}
|
||||
reasoningID: Schema.String,
|
||||
},
|
||||
})
|
||||
export type Started = Schema.Schema.Type<typeof Started>
|
||||
|
||||
export class Delta extends Schema.Class<Delta>("Session.Event.Reasoning.Delta")({
|
||||
export const Delta = Event.define({
|
||||
type: "session.next.reasoning.delta",
|
||||
aggregate: "sessionID",
|
||||
schema: {
|
||||
...Base,
|
||||
type: Schema.Literal("reasoning.delta"),
|
||||
reasoningID: Schema.String,
|
||||
delta: Schema.String,
|
||||
}) {
|
||||
static create(input: BaseInput & { delta: string }) {
|
||||
return new Delta({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "reasoning.delta",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
delta: input.delta,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
export type Delta = Schema.Schema.Type<typeof Delta>
|
||||
|
||||
export class Ended extends Schema.Class<Ended>("Session.Event.Reasoning.Ended")({
|
||||
export const Ended = Event.define({
|
||||
type: "session.next.reasoning.ended",
|
||||
aggregate: "sessionID",
|
||||
schema: {
|
||||
...Base,
|
||||
type: Schema.Literal("reasoning.ended"),
|
||||
reasoningID: Schema.String,
|
||||
text: Schema.String,
|
||||
}) {
|
||||
static create(input: BaseInput & { text: string }) {
|
||||
return new Ended({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "reasoning.ended",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
text: input.text,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
export type Ended = Schema.Schema.Type<typeof Ended>
|
||||
}
|
||||
|
||||
export namespace Tool {
|
||||
export namespace Input {
|
||||
export class Started extends Schema.Class<Started>("Session.Event.Tool.Input.Started")({
|
||||
export namespace Tool {
|
||||
export namespace Input {
|
||||
export const Started = Event.define({
|
||||
type: "session.next.tool.input.started",
|
||||
aggregate: "sessionID",
|
||||
schema: {
|
||||
...Base,
|
||||
callID: Schema.String,
|
||||
name: Schema.String,
|
||||
type: Schema.Literal("tool.input.started"),
|
||||
}) {
|
||||
static create(input: BaseInput & { callID: string; name: string }) {
|
||||
return new Started({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "tool.input.started",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
callID: input.callID,
|
||||
name: input.name,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
export type Started = Schema.Schema.Type<typeof Started>
|
||||
|
||||
export class Delta extends Schema.Class<Delta>("Session.Event.Tool.Input.Delta")({
|
||||
export const Delta = Event.define({
|
||||
type: "session.next.tool.input.delta",
|
||||
aggregate: "sessionID",
|
||||
schema: {
|
||||
...Base,
|
||||
callID: Schema.String,
|
||||
type: Schema.Literal("tool.input.delta"),
|
||||
delta: Schema.String,
|
||||
}) {
|
||||
static create(input: BaseInput & { callID: string; delta: string }) {
|
||||
return new Delta({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "tool.input.delta",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
callID: input.callID,
|
||||
delta: input.delta,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
export type Delta = Schema.Schema.Type<typeof Delta>
|
||||
|
||||
export class Ended extends Schema.Class<Ended>("Session.Event.Tool.Input.Ended")({
|
||||
export const Ended = Event.define({
|
||||
type: "session.next.tool.input.ended",
|
||||
aggregate: "sessionID",
|
||||
schema: {
|
||||
...Base,
|
||||
callID: Schema.String,
|
||||
type: Schema.Literal("tool.input.ended"),
|
||||
text: Schema.String,
|
||||
}) {
|
||||
static create(input: BaseInput & { callID: string; text: string }) {
|
||||
return new Ended({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "tool.input.ended",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
callID: input.callID,
|
||||
text: input.text,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
export type Ended = Schema.Schema.Type<typeof Ended>
|
||||
}
|
||||
|
||||
export class Called extends Schema.Class<Called>("Session.Event.Tool.Called")({
|
||||
export const Called = Event.define({
|
||||
type: "session.next.tool.called",
|
||||
aggregate: "sessionID",
|
||||
schema: {
|
||||
...Base,
|
||||
type: Schema.Literal("tool.called"),
|
||||
callID: Schema.String,
|
||||
tool: Schema.String,
|
||||
input: Schema.Record(Schema.String, Schema.Unknown),
|
||||
@@ -311,148 +218,153 @@ export namespace SessionEvent {
|
||||
executed: Schema.Boolean,
|
||||
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
}),
|
||||
}) {
|
||||
static create(
|
||||
input: BaseInput & {
|
||||
callID: string
|
||||
tool: string
|
||||
input: Record<string, unknown>
|
||||
provider: Called["provider"]
|
||||
},
|
||||
) {
|
||||
return new Called({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "tool.called",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
callID: input.callID,
|
||||
tool: input.tool,
|
||||
input: input.input,
|
||||
provider: input.provider,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
export type Called = Schema.Schema.Type<typeof Called>
|
||||
|
||||
export class Success extends Schema.Class<Success>("Session.Event.Tool.Success")({
|
||||
export const Progress = Event.define({
|
||||
type: "session.next.tool.progress",
|
||||
aggregate: "sessionID",
|
||||
schema: {
|
||||
...Base,
|
||||
type: Schema.Literal("tool.success"),
|
||||
callID: Schema.String,
|
||||
title: Schema.String,
|
||||
output: Schema.String.pipe(Schema.optional),
|
||||
attachments: Schema.Array(FileAttachment).pipe(Schema.optional),
|
||||
structured: ToolOutput.Structured,
|
||||
content: Schema.Array(ToolOutput.Content),
|
||||
},
|
||||
})
|
||||
export type Progress = Schema.Schema.Type<typeof Progress>
|
||||
|
||||
export const Success = Event.define({
|
||||
type: "session.next.tool.success",
|
||||
aggregate: "sessionID",
|
||||
schema: {
|
||||
...Base,
|
||||
callID: Schema.String,
|
||||
structured: ToolOutput.Structured,
|
||||
content: Schema.Array(ToolOutput.Content),
|
||||
provider: Schema.Struct({
|
||||
executed: Schema.Boolean,
|
||||
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
}),
|
||||
}) {
|
||||
static create(
|
||||
input: BaseInput & {
|
||||
callID: string
|
||||
title: string
|
||||
output?: string
|
||||
attachments?: FileAttachment[]
|
||||
provider: Success["provider"]
|
||||
},
|
||||
) {
|
||||
return new Success({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "tool.success",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
callID: input.callID,
|
||||
title: input.title,
|
||||
output: input.output,
|
||||
attachments: input.attachments,
|
||||
provider: input.provider,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
export type Success = Schema.Schema.Type<typeof Success>
|
||||
|
||||
export class Error extends Schema.Class<Error>("Session.Event.Tool.Error")({
|
||||
export const Error = Event.define({
|
||||
type: "session.next.tool.error",
|
||||
aggregate: "sessionID",
|
||||
schema: {
|
||||
...Base,
|
||||
type: Schema.Literal("tool.error"),
|
||||
callID: Schema.String,
|
||||
error: Schema.String,
|
||||
error: Schema.Struct({
|
||||
type: Schema.String,
|
||||
message: Schema.String,
|
||||
}),
|
||||
provider: Schema.Struct({
|
||||
executed: Schema.Boolean,
|
||||
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
}),
|
||||
}) {
|
||||
static create(input: BaseInput & { callID: string; error: string; provider: Error["provider"] }) {
|
||||
return new Error({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "tool.error",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
callID: input.callID,
|
||||
error: input.error,
|
||||
provider: input.provider,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
export type Error = Schema.Schema.Type<typeof Error>
|
||||
}
|
||||
|
||||
export class Retried extends Schema.Class<Retried>("Session.Event.Retried")({
|
||||
export const RetryError = Schema.Struct({
|
||||
message: Schema.String,
|
||||
statusCode: NonNegativeInt.pipe(Schema.optional),
|
||||
isRetryable: Schema.Boolean,
|
||||
responseHeaders: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
|
||||
responseBody: Schema.String.pipe(Schema.optional),
|
||||
metadata: Schema.Record(Schema.String, Schema.String).pipe(Schema.optional),
|
||||
}).annotate({
|
||||
identifier: "session.next.retry_error",
|
||||
})
|
||||
export type RetryError = Schema.Schema.Type<typeof RetryError>
|
||||
|
||||
export const Retried = Event.define({
|
||||
type: "session.next.retried",
|
||||
aggregate: "sessionID",
|
||||
schema: {
|
||||
...Base,
|
||||
type: Schema.Literal("retried"),
|
||||
attempt: NonNegativeInt,
|
||||
error: RetryError,
|
||||
}) {
|
||||
static create(input: BaseInput & { attempt: number; error: RetryError }) {
|
||||
return new Retried({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "retried",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
attempt: input.attempt,
|
||||
error: input.error,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
export type Retried = Schema.Schema.Type<typeof Retried>
|
||||
|
||||
export class Compacted extends Schema.Class<Compacted>("Session.Event.Compated")({
|
||||
...Base,
|
||||
type: Schema.Literal("compacted"),
|
||||
auto: Schema.Boolean,
|
||||
overflow: Schema.Boolean.pipe(Schema.optional),
|
||||
}) {
|
||||
static create(input: BaseInput & { auto: boolean; overflow?: boolean }) {
|
||||
return new Compacted({
|
||||
id: input.id ?? ID.create(),
|
||||
type: "compacted",
|
||||
timestamp: input.timestamp ?? DateTime.makeUnsafe(Date.now()),
|
||||
metadata: input.metadata,
|
||||
auto: input.auto,
|
||||
overflow: input.overflow,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const Event = Schema.Union(
|
||||
[
|
||||
Prompt,
|
||||
Synthetic,
|
||||
Step.Started,
|
||||
Step.Ended,
|
||||
Text.Started,
|
||||
Text.Delta,
|
||||
Text.Ended,
|
||||
Tool.Input.Started,
|
||||
Tool.Input.Delta,
|
||||
Tool.Input.Ended,
|
||||
Tool.Called,
|
||||
Tool.Success,
|
||||
Tool.Error,
|
||||
Reasoning.Started,
|
||||
Reasoning.Delta,
|
||||
Reasoning.Ended,
|
||||
Retried,
|
||||
Compacted,
|
||||
],
|
||||
{
|
||||
mode: "oneOf",
|
||||
export namespace Compaction {
|
||||
export const Started = Event.define({
|
||||
type: "session.next.compaction.started",
|
||||
aggregate: "sessionID",
|
||||
schema: {
|
||||
...Base,
|
||||
reason: Schema.Union([Schema.Literal("auto"), Schema.Literal("manual")]),
|
||||
},
|
||||
).pipe(Schema.toTaggedUnion("type"))
|
||||
export type Event = Schema.Schema.Type<typeof Event>
|
||||
export type Type = Event["type"]
|
||||
})
|
||||
export type Started = Schema.Schema.Type<typeof Started>
|
||||
|
||||
export const Delta = Event.define({
|
||||
type: "session.next.compaction.delta",
|
||||
aggregate: "sessionID",
|
||||
schema: {
|
||||
...Base,
|
||||
text: Schema.String,
|
||||
},
|
||||
})
|
||||
|
||||
export const Ended = Event.define({
|
||||
type: "session.next.compaction.ended",
|
||||
aggregate: "sessionID",
|
||||
schema: {
|
||||
...Base,
|
||||
text: Schema.String,
|
||||
include: Schema.String.pipe(Schema.optional),
|
||||
},
|
||||
})
|
||||
export type Ended = Schema.Schema.Type<typeof Ended>
|
||||
}
|
||||
|
||||
export const All = Schema.Union(
|
||||
[
|
||||
AgentSwitched,
|
||||
ModelSwitched,
|
||||
Prompted,
|
||||
Synthetic,
|
||||
Step.Started,
|
||||
Step.Ended,
|
||||
Text.Started,
|
||||
Text.Delta,
|
||||
Text.Ended,
|
||||
Tool.Input.Started,
|
||||
Tool.Input.Delta,
|
||||
Tool.Input.Ended,
|
||||
Tool.Called,
|
||||
Tool.Progress,
|
||||
Tool.Success,
|
||||
Tool.Error,
|
||||
Reasoning.Started,
|
||||
Reasoning.Delta,
|
||||
Reasoning.Ended,
|
||||
Retried,
|
||||
Compaction.Started,
|
||||
Compaction.Delta,
|
||||
Compaction.Ended,
|
||||
],
|
||||
{
|
||||
mode: "oneOf",
|
||||
},
|
||||
).pipe(Schema.toTaggedUnion("type"))
|
||||
|
||||
// user
|
||||
// assistant
|
||||
// assistant
|
||||
// assistant
|
||||
// user
|
||||
// compaction marker
|
||||
// -> text
|
||||
// assistant
|
||||
|
||||
export type Event = Schema.Schema.Type<typeof All>
|
||||
export type Type = Event["type"]
|
||||
|
||||
export * as SessionEvent from "./session-event"
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
import { produce, type WritableDraft } from "immer"
|
||||
import { SessionEvent } from "./session-event"
|
||||
import { SessionMessage } from "./session-message"
|
||||
|
||||
export type MemoryState = {
|
||||
messages: SessionMessage.Message[]
|
||||
}
|
||||
|
||||
export interface Adapter<Result> {
|
||||
readonly getCurrentAssistant: () => SessionMessage.Assistant | undefined
|
||||
readonly getCurrentCompaction: () => SessionMessage.Compaction | undefined
|
||||
readonly updateAssistant: (assistant: SessionMessage.Assistant) => void
|
||||
readonly updateCompaction: (compaction: SessionMessage.Compaction) => void
|
||||
readonly appendMessage: (message: SessionMessage.Message) => void
|
||||
readonly finish: () => Result
|
||||
}
|
||||
|
||||
export function memory(state: MemoryState): Adapter<MemoryState> {
|
||||
const activeAssistantIndex = () =>
|
||||
state.messages.findLastIndex((message) => message.type === "assistant" && !message.time.completed)
|
||||
const activeCompactionIndex = () => state.messages.findLastIndex((message) => message.type === "compaction")
|
||||
|
||||
return {
|
||||
getCurrentAssistant() {
|
||||
const index = activeAssistantIndex()
|
||||
if (index < 0) return
|
||||
const assistant = state.messages[index]
|
||||
return assistant?.type === "assistant" ? assistant : undefined
|
||||
},
|
||||
getCurrentCompaction() {
|
||||
const index = activeCompactionIndex()
|
||||
if (index < 0) return
|
||||
const compaction = state.messages[index]
|
||||
return compaction?.type === "compaction" ? compaction : undefined
|
||||
},
|
||||
updateAssistant(assistant) {
|
||||
const index = activeAssistantIndex()
|
||||
if (index < 0) return
|
||||
const current = state.messages[index]
|
||||
if (current?.type !== "assistant") return
|
||||
state.messages[index] = assistant
|
||||
},
|
||||
updateCompaction(compaction) {
|
||||
const index = activeCompactionIndex()
|
||||
if (index < 0) return
|
||||
const current = state.messages[index]
|
||||
if (current?.type !== "compaction") return
|
||||
state.messages[index] = compaction
|
||||
},
|
||||
appendMessage(message) {
|
||||
state.messages.push(message)
|
||||
},
|
||||
finish() {
|
||||
return state
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function update<Result>(adapter: Adapter<Result>, event: SessionEvent.Event): Result {
|
||||
const currentAssistant = adapter.getCurrentAssistant()
|
||||
type DraftAssistant = WritableDraft<SessionMessage.Assistant>
|
||||
type DraftTool = WritableDraft<SessionMessage.AssistantTool>
|
||||
type DraftText = WritableDraft<SessionMessage.AssistantText>
|
||||
type DraftReasoning = WritableDraft<SessionMessage.AssistantReasoning>
|
||||
|
||||
const latestTool = (assistant: DraftAssistant | undefined, callID?: string) =>
|
||||
assistant?.content.findLast(
|
||||
(item): item is DraftTool => item.type === "tool" && (callID === undefined || item.id === callID),
|
||||
)
|
||||
|
||||
const latestText = (assistant: DraftAssistant | undefined) =>
|
||||
assistant?.content.findLast((item): item is DraftText => item.type === "text")
|
||||
|
||||
const latestReasoning = (assistant: DraftAssistant | undefined, reasoningID: string) =>
|
||||
assistant?.content.findLast(
|
||||
(item): item is DraftReasoning => item.type === "reasoning" && item.id === reasoningID,
|
||||
)
|
||||
|
||||
SessionEvent.All.match(event, {
|
||||
"session.next.agent.switched": (event) => {
|
||||
adapter.appendMessage(SessionMessage.AgentSwitched.fromEvent(event))
|
||||
},
|
||||
"session.next.model.switched": (event) => {
|
||||
adapter.appendMessage(SessionMessage.ModelSwitched.fromEvent(event))
|
||||
},
|
||||
"session.next.prompted": (event) => {
|
||||
adapter.appendMessage(SessionMessage.User.fromEvent(event))
|
||||
},
|
||||
"session.next.synthetic": (event) => {
|
||||
adapter.appendMessage(SessionMessage.Synthetic.fromEvent(event))
|
||||
},
|
||||
"session.next.step.started": (event) => {
|
||||
if (currentAssistant) {
|
||||
adapter.updateAssistant(
|
||||
produce(currentAssistant, (draft) => {
|
||||
draft.time.completed = event.data.timestamp
|
||||
}),
|
||||
)
|
||||
}
|
||||
adapter.appendMessage(SessionMessage.Assistant.fromEvent(event))
|
||||
},
|
||||
"session.next.step.ended": (event) => {
|
||||
if (currentAssistant) {
|
||||
adapter.updateAssistant(
|
||||
produce(currentAssistant, (draft) => {
|
||||
draft.time.completed = event.data.timestamp
|
||||
draft.finish = event.data.finish
|
||||
draft.cost = event.data.cost
|
||||
draft.tokens = event.data.tokens
|
||||
if (event.data.snapshot) draft.snapshot = { ...draft.snapshot, end: event.data.snapshot }
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
"session.next.text.started": () => {
|
||||
if (currentAssistant) {
|
||||
adapter.updateAssistant(
|
||||
produce(currentAssistant, (draft) => {
|
||||
draft.content.push({
|
||||
type: "text",
|
||||
text: "",
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
"session.next.text.delta": (event) => {
|
||||
if (currentAssistant) {
|
||||
adapter.updateAssistant(
|
||||
produce(currentAssistant, (draft) => {
|
||||
const match = latestText(draft)
|
||||
if (match) match.text += event.data.delta
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
"session.next.text.ended": (event) => {
|
||||
if (currentAssistant) {
|
||||
adapter.updateAssistant(
|
||||
produce(currentAssistant, (draft) => {
|
||||
const match = latestText(draft)
|
||||
if (match) match.text = event.data.text
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
"session.next.tool.input.started": (event) => {
|
||||
if (currentAssistant) {
|
||||
adapter.updateAssistant(
|
||||
produce(currentAssistant, (draft) => {
|
||||
draft.content.push({
|
||||
type: "tool",
|
||||
id: event.data.callID,
|
||||
name: event.data.name,
|
||||
time: {
|
||||
created: event.data.timestamp,
|
||||
},
|
||||
state: {
|
||||
status: "pending",
|
||||
input: "",
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
"session.next.tool.input.delta": (event) => {
|
||||
if (currentAssistant) {
|
||||
adapter.updateAssistant(
|
||||
produce(currentAssistant, (draft) => {
|
||||
const match = latestTool(draft, event.data.callID)
|
||||
// oxlint-disable-next-line no-base-to-string -- event.delta is a Schema.String (runtime string)
|
||||
if (match && match.state.status === "pending") match.state.input += event.data.delta
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
"session.next.tool.input.ended": () => {},
|
||||
"session.next.tool.called": (event) => {
|
||||
if (currentAssistant) {
|
||||
adapter.updateAssistant(
|
||||
produce(currentAssistant, (draft) => {
|
||||
const match = latestTool(draft, event.data.callID)
|
||||
if (match) {
|
||||
match.provider = event.data.provider
|
||||
match.time.ran = event.data.timestamp
|
||||
match.state = {
|
||||
status: "running",
|
||||
input: event.data.input,
|
||||
structured: {},
|
||||
content: [],
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
"session.next.tool.progress": (event) => {
|
||||
if (currentAssistant) {
|
||||
adapter.updateAssistant(
|
||||
produce(currentAssistant, (draft) => {
|
||||
const match = latestTool(draft, event.data.callID)
|
||||
if (match && match.state.status === "running") {
|
||||
match.state.structured = event.data.structured
|
||||
match.state.content = [...event.data.content]
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
"session.next.tool.success": (event) => {
|
||||
if (currentAssistant) {
|
||||
adapter.updateAssistant(
|
||||
produce(currentAssistant, (draft) => {
|
||||
const match = latestTool(draft, event.data.callID)
|
||||
if (match && match.state.status === "running") {
|
||||
match.provider = event.data.provider
|
||||
match.time.completed = event.data.timestamp
|
||||
match.state = {
|
||||
status: "completed",
|
||||
input: match.state.input,
|
||||
structured: event.data.structured,
|
||||
content: [...event.data.content],
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
"session.next.tool.error": (event) => {
|
||||
if (currentAssistant) {
|
||||
adapter.updateAssistant(
|
||||
produce(currentAssistant, (draft) => {
|
||||
const match = latestTool(draft, event.data.callID)
|
||||
if (match && match.state.status === "running") {
|
||||
match.provider = event.data.provider
|
||||
match.time.completed = event.data.timestamp
|
||||
match.state = {
|
||||
status: "error",
|
||||
error: event.data.error,
|
||||
input: match.state.input,
|
||||
structured: match.state.structured,
|
||||
content: match.state.content,
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
"session.next.reasoning.started": (event) => {
|
||||
if (currentAssistant) {
|
||||
adapter.updateAssistant(
|
||||
produce(currentAssistant, (draft) => {
|
||||
draft.content.push({
|
||||
type: "reasoning",
|
||||
id: event.data.reasoningID,
|
||||
text: "",
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
"session.next.reasoning.delta": (event) => {
|
||||
if (currentAssistant) {
|
||||
adapter.updateAssistant(
|
||||
produce(currentAssistant, (draft) => {
|
||||
const match = latestReasoning(draft, event.data.reasoningID)
|
||||
if (match) match.text += event.data.delta
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
"session.next.reasoning.ended": (event) => {
|
||||
if (currentAssistant) {
|
||||
adapter.updateAssistant(
|
||||
produce(currentAssistant, (draft) => {
|
||||
const match = latestReasoning(draft, event.data.reasoningID)
|
||||
if (match) match.text = event.data.text
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
"session.next.retried": () => {},
|
||||
"session.next.compaction.started": (event) => {
|
||||
adapter.appendMessage(SessionMessage.Compaction.fromEvent(event))
|
||||
},
|
||||
"session.next.compaction.delta": (event) => {
|
||||
const currentCompaction = adapter.getCurrentCompaction()
|
||||
if (currentCompaction) {
|
||||
adapter.updateCompaction(
|
||||
produce(currentCompaction, (draft) => {
|
||||
draft.summary += event.data.text
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
"session.next.compaction.ended": (event) => {
|
||||
const currentCompaction = adapter.getCurrentCompaction()
|
||||
if (currentCompaction) {
|
||||
adapter.updateCompaction(
|
||||
produce(currentCompaction, (draft) => {
|
||||
draft.summary = event.data.text
|
||||
draft.include = event.data.include
|
||||
}),
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return adapter.finish()
|
||||
}
|
||||
|
||||
export * as SessionMessageUpdater from "./session-message-updater"
|
||||
@@ -0,0 +1,236 @@
|
||||
import { Schema } from "effect"
|
||||
import { Prompt } from "./session-prompt"
|
||||
import { SessionEvent } from "./session-event"
|
||||
import { Event } from "./event"
|
||||
import { ToolOutput } from "./tool-output"
|
||||
|
||||
export const ID = Event.ID
|
||||
export type ID = Schema.Schema.Type<typeof ID>
|
||||
|
||||
const Base = {
|
||||
id: ID,
|
||||
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
time: Schema.Struct({
|
||||
created: Schema.DateTimeUtcFromMillis,
|
||||
}),
|
||||
}
|
||||
|
||||
export class AgentSwitched extends Schema.Class<AgentSwitched>("Session.Message.AgentSwitched")({
|
||||
...Base,
|
||||
type: Schema.Literal("agent-switched"),
|
||||
agent: SessionEvent.AgentSwitched.fields.data.fields.agent,
|
||||
}) {
|
||||
static fromEvent(event: SessionEvent.AgentSwitched) {
|
||||
return new AgentSwitched({
|
||||
id: event.id,
|
||||
type: "agent-switched",
|
||||
metadata: event.metadata,
|
||||
agent: event.data.agent,
|
||||
time: { created: event.data.timestamp },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class ModelSwitched extends Schema.Class<ModelSwitched>("Session.Message.ModelSwitched")({
|
||||
...Base,
|
||||
type: Schema.Literal("model-switched"),
|
||||
model: Schema.Struct({
|
||||
id: SessionEvent.ModelSwitched.fields.data.fields.id,
|
||||
providerID: SessionEvent.ModelSwitched.fields.data.fields.providerID,
|
||||
variant: SessionEvent.ModelSwitched.fields.data.fields.variant,
|
||||
}),
|
||||
}) {
|
||||
static fromEvent(event: SessionEvent.ModelSwitched) {
|
||||
return new ModelSwitched({
|
||||
id: event.id,
|
||||
type: "model-switched",
|
||||
metadata: event.metadata,
|
||||
model: {
|
||||
id: event.data.id,
|
||||
providerID: event.data.providerID,
|
||||
variant: event.data.variant,
|
||||
},
|
||||
time: { created: event.data.timestamp },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class User extends Schema.Class<User>("Session.Message.User")({
|
||||
...Base,
|
||||
text: Prompt.fields.text,
|
||||
files: Prompt.fields.files,
|
||||
agents: Prompt.fields.agents,
|
||||
type: Schema.Literal("user"),
|
||||
time: Schema.Struct({
|
||||
created: Schema.DateTimeUtcFromMillis,
|
||||
}),
|
||||
}) {
|
||||
static fromEvent(event: SessionEvent.Prompted) {
|
||||
return new User({
|
||||
id: event.id,
|
||||
type: "user",
|
||||
metadata: event.metadata,
|
||||
text: event.data.prompt.text,
|
||||
files: event.data.prompt.files,
|
||||
agents: event.data.prompt.agents,
|
||||
time: { created: event.data.timestamp },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class Synthetic extends Schema.Class<Synthetic>("Session.Message.Synthetic")({
|
||||
...Base,
|
||||
sessionID: SessionEvent.Synthetic.fields.data.fields.sessionID,
|
||||
text: SessionEvent.Synthetic.fields.data.fields.text,
|
||||
type: Schema.Literal("synthetic"),
|
||||
}) {
|
||||
static fromEvent(event: SessionEvent.Synthetic) {
|
||||
return new Synthetic({
|
||||
sessionID: event.data.sessionID,
|
||||
text: event.data.text,
|
||||
id: event.id,
|
||||
type: "synthetic",
|
||||
time: { created: event.data.timestamp },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class ToolStatePending extends Schema.Class<ToolStatePending>("Session.Message.ToolState.Pending")({
|
||||
status: Schema.Literal("pending"),
|
||||
input: Schema.String,
|
||||
}) {}
|
||||
|
||||
export class ToolStateRunning extends Schema.Class<ToolStateRunning>("Session.Message.ToolState.Running")({
|
||||
status: Schema.Literal("running"),
|
||||
input: Schema.Record(Schema.String, Schema.Unknown),
|
||||
structured: ToolOutput.Structured,
|
||||
content: ToolOutput.Content.pipe(Schema.Array),
|
||||
}) {}
|
||||
|
||||
export class ToolStateCompleted extends Schema.Class<ToolStateCompleted>("Session.Message.ToolState.Completed")({
|
||||
status: Schema.Literal("completed"),
|
||||
input: Schema.Record(Schema.String, Schema.Unknown),
|
||||
attachments: SessionEvent.FileAttachment.pipe(Schema.Array, Schema.optional),
|
||||
content: ToolOutput.Content.pipe(Schema.Array),
|
||||
structured: ToolOutput.Structured,
|
||||
}) {}
|
||||
|
||||
export class ToolStateError extends Schema.Class<ToolStateError>("Session.Message.ToolState.Error")({
|
||||
status: Schema.Literal("error"),
|
||||
input: Schema.Record(Schema.String, Schema.Unknown),
|
||||
content: ToolOutput.Content.pipe(Schema.Array),
|
||||
structured: ToolOutput.Structured,
|
||||
error: Schema.Struct({
|
||||
type: Schema.String,
|
||||
message: Schema.String,
|
||||
}),
|
||||
}) {}
|
||||
|
||||
export const ToolState = Schema.Union([ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]).pipe(
|
||||
Schema.toTaggedUnion("status"),
|
||||
)
|
||||
export type ToolState = Schema.Schema.Type<typeof ToolState>
|
||||
|
||||
export class AssistantTool extends Schema.Class<AssistantTool>("Session.Message.Assistant.Tool")({
|
||||
type: Schema.Literal("tool"),
|
||||
id: Schema.String,
|
||||
name: Schema.String,
|
||||
provider: Schema.Struct({
|
||||
executed: Schema.Boolean,
|
||||
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
}).pipe(Schema.optional),
|
||||
state: ToolState,
|
||||
time: Schema.Struct({
|
||||
created: Schema.DateTimeUtcFromMillis,
|
||||
ran: Schema.DateTimeUtcFromMillis.pipe(Schema.optional),
|
||||
completed: Schema.DateTimeUtcFromMillis.pipe(Schema.optional),
|
||||
pruned: Schema.DateTimeUtcFromMillis.pipe(Schema.optional),
|
||||
}),
|
||||
}) {}
|
||||
|
||||
export class AssistantText extends Schema.Class<AssistantText>("Session.Message.Assistant.Text")({
|
||||
type: Schema.Literal("text"),
|
||||
text: Schema.String,
|
||||
}) {}
|
||||
|
||||
export class AssistantReasoning extends Schema.Class<AssistantReasoning>("Session.Message.Assistant.Reasoning")({
|
||||
type: Schema.Literal("reasoning"),
|
||||
id: Schema.String,
|
||||
text: Schema.String,
|
||||
}) {}
|
||||
|
||||
export const AssistantContent = Schema.Union([AssistantText, AssistantReasoning, AssistantTool]).pipe(
|
||||
Schema.toTaggedUnion("type"),
|
||||
)
|
||||
export type AssistantContent = Schema.Schema.Type<typeof AssistantContent>
|
||||
|
||||
export class Assistant extends Schema.Class<Assistant>("Session.Message.Assistant")({
|
||||
...Base,
|
||||
type: Schema.Literal("assistant"),
|
||||
agent: Schema.String,
|
||||
model: SessionEvent.Step.Started.fields.data.fields.model,
|
||||
content: AssistantContent.pipe(Schema.Array),
|
||||
snapshot: Schema.Struct({
|
||||
start: Schema.String.pipe(Schema.optional),
|
||||
end: Schema.String.pipe(Schema.optional),
|
||||
}).pipe(Schema.optional),
|
||||
finish: Schema.String.pipe(Schema.optional),
|
||||
cost: Schema.Number.pipe(Schema.optional),
|
||||
tokens: Schema.Struct({
|
||||
input: Schema.Number,
|
||||
output: Schema.Number,
|
||||
reasoning: Schema.Number,
|
||||
cache: Schema.Struct({
|
||||
read: Schema.Number,
|
||||
write: Schema.Number,
|
||||
}),
|
||||
}).pipe(Schema.optional),
|
||||
error: Schema.String.pipe(Schema.optional),
|
||||
time: Schema.Struct({
|
||||
created: Schema.DateTimeUtcFromMillis,
|
||||
completed: Schema.DateTimeUtcFromMillis.pipe(Schema.optional),
|
||||
}),
|
||||
}) {
|
||||
static fromEvent(event: SessionEvent.Step.Started) {
|
||||
return new Assistant({
|
||||
id: event.id,
|
||||
type: "assistant",
|
||||
agent: event.data.agent,
|
||||
model: event.data.model,
|
||||
time: {
|
||||
created: event.data.timestamp,
|
||||
},
|
||||
content: [],
|
||||
snapshot: event.data.snapshot ? { start: event.data.snapshot } : undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class Compaction extends Schema.Class<Compaction>("Session.Message.Compaction")({
|
||||
type: Schema.Literal("compaction"),
|
||||
reason: SessionEvent.Compaction.Started.fields.data.fields.reason,
|
||||
summary: Schema.String,
|
||||
include: Schema.String.pipe(Schema.optional),
|
||||
...Base,
|
||||
}) {
|
||||
static fromEvent(event: SessionEvent.Compaction.Started) {
|
||||
return new Compaction({
|
||||
id: event.id,
|
||||
type: "compaction",
|
||||
metadata: event.metadata,
|
||||
reason: event.data.reason,
|
||||
summary: "",
|
||||
time: { created: event.data.timestamp },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const Message = Schema.Union([AgentSwitched, ModelSwitched, User, Synthetic, Assistant, Compaction])
|
||||
.pipe(Schema.toTaggedUnion("type"))
|
||||
.annotate({ identifier: "Session.Message" })
|
||||
|
||||
export type Message = Schema.Schema.Type<typeof Message>
|
||||
|
||||
export type Type = Message["type"]
|
||||
|
||||
export * as SessionMessage from "./session-message"
|
||||
@@ -0,0 +1,36 @@
|
||||
import * as Schema from "effect/Schema"
|
||||
|
||||
export class Source extends Schema.Class<Source>("Prompt.Source")({
|
||||
start: Schema.Number,
|
||||
end: Schema.Number,
|
||||
text: Schema.String,
|
||||
}) {}
|
||||
|
||||
export class FileAttachment extends Schema.Class<FileAttachment>("Prompt.FileAttachment")({
|
||||
uri: Schema.String,
|
||||
mime: Schema.String,
|
||||
name: Schema.String.pipe(Schema.optional),
|
||||
description: Schema.String.pipe(Schema.optional),
|
||||
source: Source.pipe(Schema.optional),
|
||||
}) {
|
||||
static create(input: FileAttachment) {
|
||||
return new FileAttachment({
|
||||
uri: input.uri,
|
||||
mime: input.mime,
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
source: input.source,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class AgentAttachment extends Schema.Class<AgentAttachment>("Prompt.AgentAttachment")({
|
||||
name: Schema.String,
|
||||
source: Source.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class Prompt extends Schema.Class<Prompt>("Prompt")({
|
||||
text: Schema.String,
|
||||
files: Schema.Array(FileAttachment).pipe(Schema.optional),
|
||||
agents: Schema.Array(AgentAttachment).pipe(Schema.optional),
|
||||
}) {}
|
||||
@@ -1,69 +1,242 @@
|
||||
import { Context, Layer, Schema, Effect } from "effect"
|
||||
import { SessionEntry } from "./session-entry"
|
||||
import { Struct } from "effect"
|
||||
import { Session } from "@/session/session"
|
||||
import { SessionMessageTable, SessionTable } from "@/session/session.sql"
|
||||
import { SessionID } from "@/session/schema"
|
||||
import { WorkspaceID } from "@/control-plane/schema"
|
||||
import { and, asc, desc, eq, gt, gte, isNull, like, lt, or, type SQL } from "@/storage/db"
|
||||
import * as Database from "@/storage/db"
|
||||
import { Context, DateTime, Effect, Layer, Schema } from "effect"
|
||||
import { SessionMessage } from "./session-message"
|
||||
import type { Prompt } from "./session-prompt"
|
||||
import type { Event } from "./event"
|
||||
import { ProjectID } from "@/project/schema"
|
||||
import { ModelID, ProviderID } from "@/provider/schema"
|
||||
import { SessionEvent } from "./session-event"
|
||||
import { SyncEvent } from "@/sync"
|
||||
|
||||
export const ID = SessionID
|
||||
export const Delivery = Schema.Union([Schema.Literal("immediate"), Schema.Literal("deferred")]).annotate({
|
||||
identifier: "Session.Delivery",
|
||||
})
|
||||
export type Delivery = Schema.Schema.Type<typeof Delivery>
|
||||
|
||||
export type ID = Schema.Schema.Type<typeof ID>
|
||||
|
||||
export class PromptInput extends Schema.Class<PromptInput>("Session.PromptInput")({
|
||||
...Struct.omit(SessionEntry.User.fields, ["time", "type"]),
|
||||
id: Schema.optionalKey(SessionEntry.ID),
|
||||
sessionID: ID,
|
||||
}) {}
|
||||
|
||||
export class CreateInput extends Schema.Class<CreateInput>("Session.CreateInput")({
|
||||
id: Schema.optionalKey(ID),
|
||||
}) {}
|
||||
export const DefaultDelivery = "immediate" satisfies Delivery
|
||||
|
||||
export class Info extends Schema.Class<Info>("Session.Info")({
|
||||
id: ID,
|
||||
id: SessionID,
|
||||
parentID: SessionID.pipe(Schema.optional),
|
||||
projectID: ProjectID,
|
||||
workspaceID: WorkspaceID.pipe(Schema.optional),
|
||||
path: Schema.String.pipe(Schema.optional),
|
||||
agent: Schema.String.pipe(Schema.optional),
|
||||
model: Schema.Struct({
|
||||
id: Schema.String,
|
||||
providerID: Schema.String,
|
||||
modelID: Schema.String,
|
||||
id: ModelID,
|
||||
providerID: ProviderID,
|
||||
variant: Schema.String.pipe(Schema.optional),
|
||||
}).pipe(Schema.optional),
|
||||
time: Schema.Struct({
|
||||
created: Schema.DateTimeUtcFromMillis,
|
||||
updated: Schema.DateTimeUtcFromMillis,
|
||||
archived: Schema.DateTimeUtcFromMillis.pipe(Schema.optional),
|
||||
}),
|
||||
title: Schema.String,
|
||||
/*
|
||||
slug: Schema.String,
|
||||
directory: Schema.String,
|
||||
path: optionalOmitUndefined(Schema.String),
|
||||
parentID: optionalOmitUndefined(SessionID),
|
||||
summary: optionalOmitUndefined(Summary),
|
||||
share: optionalOmitUndefined(Share),
|
||||
title: Schema.String,
|
||||
version: Schema.String,
|
||||
time: Time,
|
||||
permission: optionalOmitUndefined(Permission.Ruleset),
|
||||
revert: optionalOmitUndefined(Revert),
|
||||
*/
|
||||
}) {}
|
||||
|
||||
export interface Interface {
|
||||
fromID: (id: ID) => Effect.Effect<Info>
|
||||
create: (input: CreateInput) => Effect.Effect<Info>
|
||||
prompt: (input: PromptInput) => Effect.Effect<SessionEntry.User>
|
||||
readonly list: (input: {
|
||||
limit?: number
|
||||
order?: "asc" | "desc"
|
||||
directory?: string
|
||||
path?: string
|
||||
workspaceID?: WorkspaceID
|
||||
roots?: boolean
|
||||
start?: number
|
||||
search?: string
|
||||
cursor?: {
|
||||
id: SessionID
|
||||
time: number
|
||||
direction: "previous" | "next"
|
||||
}
|
||||
}) => Effect.Effect<Info[], never>
|
||||
readonly messages: (input: {
|
||||
sessionID: SessionID
|
||||
limit?: number
|
||||
order?: "asc" | "desc"
|
||||
cursor?: {
|
||||
id: SessionMessage.ID
|
||||
time: number
|
||||
direction: "previous" | "next"
|
||||
}
|
||||
}) => Effect.Effect<SessionMessage.Message[], never>
|
||||
readonly prompt: (input: {
|
||||
id?: Event.ID
|
||||
sessionID: SessionID
|
||||
prompt: Prompt
|
||||
delivery?: Delivery
|
||||
}) => Effect.Effect<SessionMessage.User, never>
|
||||
readonly switchAgent: (input: { sessionID: SessionID; agent: string }) => Effect.Effect<void, never>
|
||||
readonly switchModel: (input: {
|
||||
sessionID: SessionID
|
||||
id: ModelID
|
||||
providerID: ProviderID
|
||||
variant?: string
|
||||
}) => Effect.Effect<void, never>
|
||||
readonly compact: (sessionID: SessionID) => Effect.Effect<void, never>
|
||||
readonly wait: (sessionID: SessionID) => Effect.Effect<void, never>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("Session.Service") {}
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Session") {}
|
||||
|
||||
export const layer = Layer.effect(Service)(
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const session = yield* Session.Service
|
||||
const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Message)
|
||||
|
||||
const create: Interface["create"] = Effect.fn("Session.create")(function* (_input) {
|
||||
throw new Error("Not implemented")
|
||||
})
|
||||
const decode = (row: typeof SessionMessageTable.$inferSelect) =>
|
||||
decodeMessage({ ...row.data, id: row.id, type: row.type })
|
||||
|
||||
const prompt: Interface["prompt"] = Effect.fn("Session.prompt")(function* (_input) {
|
||||
throw new Error("Not implemented")
|
||||
})
|
||||
function fromRow(row: typeof SessionTable.$inferSelect): Info {
|
||||
return {
|
||||
id: SessionID.make(row.id),
|
||||
projectID: ProjectID.make(row.project_id),
|
||||
workspaceID: row.workspace_id ? WorkspaceID.make(row.workspace_id) : undefined,
|
||||
title: row.title,
|
||||
parentID: row.parent_id ? SessionID.make(row.parent_id) : undefined,
|
||||
path: row.path ?? "",
|
||||
agent: row.agent ?? undefined,
|
||||
model: row.model
|
||||
? {
|
||||
id: ModelID.make(row.model.id),
|
||||
providerID: ProviderID.make(row.model.providerID),
|
||||
variant: row.model.variant,
|
||||
}
|
||||
: undefined,
|
||||
time: {
|
||||
created: DateTime.makeUnsafe(row.time_created),
|
||||
updated: DateTime.makeUnsafe(row.time_updated),
|
||||
archived: row.time_archived ? DateTime.makeUnsafe(row.time_archived) : undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const fromID: Interface["fromID"] = Effect.fn("Session.fromID")(function* (id) {
|
||||
const match = yield* session.get(id)
|
||||
return fromV1(match)
|
||||
})
|
||||
const result: Interface = {
|
||||
list: Effect.fn("V2Session.list")(function* (input) {
|
||||
const direction = input.cursor?.direction ?? "next"
|
||||
let order = input.order ?? "desc"
|
||||
// Query the adjacent rows in reverse, then flip them back into the requested order below.
|
||||
if (direction === "previous" && order === "asc") order = "desc"
|
||||
if (direction === "previous" && order === "desc") order = "asc"
|
||||
const conditions: SQL[] = []
|
||||
if (input.directory) conditions.push(eq(SessionTable.directory, input.directory))
|
||||
if (input.path)
|
||||
conditions.push(or(eq(SessionTable.path, input.path), like(SessionTable.path, `${input.path}/%`))!)
|
||||
if (input.workspaceID) conditions.push(eq(SessionTable.workspace_id, input.workspaceID))
|
||||
if (input.roots) conditions.push(isNull(SessionTable.parent_id))
|
||||
if (input.start) conditions.push(gte(SessionTable.time_created, input.start))
|
||||
if (input.search) conditions.push(like(SessionTable.title, `%${input.search}%`))
|
||||
if (input.cursor) {
|
||||
conditions.push(
|
||||
order === "asc"
|
||||
? or(
|
||||
gt(SessionTable.time_created, input.cursor.time),
|
||||
and(eq(SessionTable.time_created, input.cursor.time), gt(SessionTable.id, input.cursor.id)),
|
||||
)!
|
||||
: or(
|
||||
lt(SessionTable.time_created, input.cursor.time),
|
||||
and(eq(SessionTable.time_created, input.cursor.time), lt(SessionTable.id, input.cursor.id)),
|
||||
)!,
|
||||
)
|
||||
}
|
||||
const query = Database.Client()
|
||||
.select()
|
||||
.from(SessionTable)
|
||||
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
||||
.orderBy(
|
||||
order === "asc" ? asc(SessionTable.time_created) : desc(SessionTable.time_created),
|
||||
order === "asc" ? asc(SessionTable.id) : desc(SessionTable.id),
|
||||
)
|
||||
|
||||
return Service.of({
|
||||
create,
|
||||
prompt,
|
||||
fromID,
|
||||
})
|
||||
const rows = input.limit === undefined ? query.all() : query.limit(input.limit).all()
|
||||
return (direction === "previous" ? rows.toReversed() : rows).map((row) => fromRow(row))
|
||||
}),
|
||||
messages: Effect.fn("V2Session.messages")(function* (input) {
|
||||
const direction = input.cursor?.direction ?? "next"
|
||||
let order = input.order ?? "desc"
|
||||
// Query the adjacent rows in reverse, then flip them back into the requested order below.
|
||||
if (direction === "previous" && order === "asc") order = "desc"
|
||||
if (direction === "previous" && order === "desc") order = "asc"
|
||||
const boundary = input.cursor
|
||||
? order === "asc"
|
||||
? or(
|
||||
gt(SessionMessageTable.time_created, input.cursor.time),
|
||||
and(
|
||||
eq(SessionMessageTable.time_created, input.cursor.time),
|
||||
gt(SessionMessageTable.id, input.cursor.id),
|
||||
),
|
||||
)
|
||||
: or(
|
||||
lt(SessionMessageTable.time_created, input.cursor.time),
|
||||
and(
|
||||
eq(SessionMessageTable.time_created, input.cursor.time),
|
||||
lt(SessionMessageTable.id, input.cursor.id),
|
||||
),
|
||||
)
|
||||
: undefined
|
||||
const where = boundary
|
||||
? and(eq(SessionMessageTable.session_id, input.sessionID), boundary)
|
||||
: eq(SessionMessageTable.session_id, input.sessionID)
|
||||
|
||||
const rows = Database.use((db) => {
|
||||
const query = db
|
||||
.select()
|
||||
.from(SessionMessageTable)
|
||||
.where(where)
|
||||
.orderBy(
|
||||
order === "asc" ? asc(SessionMessageTable.time_created) : desc(SessionMessageTable.time_created),
|
||||
order === "asc" ? asc(SessionMessageTable.id) : desc(SessionMessageTable.id),
|
||||
)
|
||||
const rows = input.limit === undefined ? query.all() : query.limit(input.limit).all()
|
||||
return direction === "previous" ? rows.toReversed() : rows
|
||||
})
|
||||
return rows.map((row) => decode(row))
|
||||
}),
|
||||
prompt: Effect.fn("V2Session.prompt")(function* (_input) {
|
||||
return {} as any
|
||||
}),
|
||||
switchAgent: Effect.fn("V2Session.switchAgent")(function* (input) {
|
||||
SyncEvent.run(SessionEvent.AgentSwitched.Sync, {
|
||||
sessionID: input.sessionID,
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
agent: input.agent,
|
||||
})
|
||||
}),
|
||||
switchModel: Effect.fn("V2Session.switchModel")(function* (input) {
|
||||
SyncEvent.run(SessionEvent.ModelSwitched.Sync, {
|
||||
sessionID: input.sessionID,
|
||||
timestamp: DateTime.makeUnsafe(Date.now()),
|
||||
id: input.id,
|
||||
providerID: input.providerID,
|
||||
variant: input.variant,
|
||||
})
|
||||
}),
|
||||
compact: Effect.fn("V2Session.compact")(function* (_sessionID) {}),
|
||||
wait: Effect.fn("V2Session.wait")(function* (_sessionID) {}),
|
||||
}
|
||||
|
||||
return Service.of(result)
|
||||
}),
|
||||
)
|
||||
|
||||
function fromV1(input: Session.Info): Info {
|
||||
return new Info({
|
||||
id: ID.make(input.id),
|
||||
})
|
||||
}
|
||||
export const defaultLayer = layer
|
||||
|
||||
export * as SessionV2 from "./session"
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
export * as ToolOutput from "./tool-output"
|
||||
import { Schema } from "effect"
|
||||
|
||||
export class TextContent extends Schema.Class<TextContent>("Tool.TextContent")({
|
||||
type: Schema.Literal("text"),
|
||||
text: Schema.String,
|
||||
}) {}
|
||||
|
||||
export class FileContent extends Schema.Class<FileContent>("Tool.FileContent")({
|
||||
type: Schema.Literal("file"),
|
||||
uri: Schema.String,
|
||||
mime: Schema.String,
|
||||
name: Schema.String.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export const Content = Schema.Union([TextContent, FileContent]).pipe(Schema.toTaggedUnion("type"))
|
||||
|
||||
export const Structured = Schema.Record(Schema.String, Schema.Any)
|
||||
@@ -58,6 +58,7 @@ function toolEvent(
|
||||
raw: opts.raw,
|
||||
}
|
||||
const payload: EventMessagePartUpdated = {
|
||||
id: `evt_${opts.callID}`,
|
||||
type: "message.part.updated",
|
||||
properties: {
|
||||
sessionID: sessionId,
|
||||
|
||||
@@ -25,6 +25,7 @@ function event(payload: Event, input: { directory: string; workspace?: string })
|
||||
|
||||
function vcs(branch: string): Event {
|
||||
return {
|
||||
id: `evt_vcs_${branch}`,
|
||||
type: "vcs.branch.updated",
|
||||
properties: {
|
||||
branch,
|
||||
@@ -34,6 +35,7 @@ function vcs(branch: string): Event {
|
||||
|
||||
function update(version: string): Event {
|
||||
return {
|
||||
id: `evt_update_${version}`,
|
||||
type: "installation.update-available",
|
||||
properties: {
|
||||
version,
|
||||
|
||||
@@ -79,7 +79,7 @@ delete process.env["OPENCODE_SERVER_USERNAME"]
|
||||
process.env["OPENCODE_DB"] = ":memory:"
|
||||
|
||||
// Now safe to import from src/
|
||||
const Log = await import("@opencode-ai/core/util/log")
|
||||
const { Log } = await import("@opencode-ai/core/util/log")
|
||||
const { initProjectors } = await import("../src/server/projectors")
|
||||
|
||||
void Log.init({
|
||||
|
||||
@@ -16,7 +16,9 @@ import { Session } from "@/session/session"
|
||||
import { MessageID, PartID, type SessionID } from "../../src/session/schema"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { Database } from "@/storage/db"
|
||||
import { SessionTable } from "@/session/session.sql"
|
||||
import { SessionMessageTable, SessionTable } from "@/session/session.sql"
|
||||
import { SessionMessage } from "../../src/v2/session-message"
|
||||
import * as DateTime from "effect/DateTime"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
@@ -202,6 +204,45 @@ describe("session HttpApi", () => {
|
||||
{ headers },
|
||||
),
|
||||
).toMatchObject({ info: { id: message.info.id } })
|
||||
|
||||
yield* Effect.promise(() =>
|
||||
Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const message = new SessionMessage.Assistant({
|
||||
id: SessionMessage.ID.create(),
|
||||
type: "assistant",
|
||||
agent: "build",
|
||||
model: { id: "model", providerID: "provider" },
|
||||
time: { created: DateTime.makeUnsafe(1) },
|
||||
content: [],
|
||||
})
|
||||
Database.use((db) =>
|
||||
db
|
||||
.insert(SessionMessageTable)
|
||||
.values([
|
||||
{
|
||||
id: message.id,
|
||||
session_id: parent.id,
|
||||
type: message.type,
|
||||
time_created: 1,
|
||||
data: {
|
||||
time: { created: 1 },
|
||||
agent: message.agent,
|
||||
model: message.model,
|
||||
content: message.content,
|
||||
} as NonNullable<typeof SessionMessageTable.$inferInsert["data"]>,
|
||||
},
|
||||
])
|
||||
.run(),
|
||||
)
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
expect(yield* requestJson<SessionMessage.Message[]>(`/api/session/${parent.id}/message`, { headers })).toMatchObject([
|
||||
{ type: "assistant" },
|
||||
])
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@ import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { MessageID, PartID, SessionID } from "../../src/session/schema"
|
||||
import { SessionStatus } from "../../src/session/status"
|
||||
import { SessionSummary } from "../../src/session/summary"
|
||||
import { SessionV2 } from "../../src/v2/session"
|
||||
import { ModelID, ProviderID } from "../../src/provider/schema"
|
||||
import type { Provider } from "@/provider/provider"
|
||||
import * as SessionProcessorModule from "../../src/session/processor"
|
||||
@@ -595,6 +596,15 @@ describe("session.compaction.create", () => {
|
||||
auto: true,
|
||||
overflow: true,
|
||||
})
|
||||
|
||||
const v2 = yield* SessionV2.Service.use((svc) => svc.messages({ sessionID: info.id })).pipe(
|
||||
Effect.provide(SessionV2.defaultLayer),
|
||||
)
|
||||
expect(v2.at(-1)).toMatchObject({
|
||||
type: "compaction",
|
||||
reason: "auto",
|
||||
summary: "",
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@ import { ModelID, ProviderID } from "../../src/provider/schema"
|
||||
import { Question } from "../../src/question"
|
||||
import { Todo } from "../../src/session/todo"
|
||||
import { Session } from "@/session/session"
|
||||
import { SessionMessageTable } from "../../src/session/session.sql"
|
||||
import { LLM } from "../../src/session/llm"
|
||||
import { MessageV2 } from "../../src/session/message-v2"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
@@ -31,6 +32,7 @@ import { SessionRevert } from "../../src/session/revert"
|
||||
import { SessionRunState } from "../../src/session/run-state"
|
||||
import { MessageID, PartID, SessionID } from "../../src/session/schema"
|
||||
import { SessionStatus } from "../../src/session/status"
|
||||
import { SessionV2 } from "../../src/v2/session"
|
||||
import { Skill } from "../../src/skill"
|
||||
import { SystemPrompt } from "../../src/session/system"
|
||||
import { Shell } from "../../src/shell/shell"
|
||||
@@ -39,6 +41,7 @@ import { ToolRegistry } from "@/tool/registry"
|
||||
import { Truncate } from "@/tool/truncate"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import * as Database from "../../src/storage/db"
|
||||
import { Ripgrep } from "../../src/file/ripgrep"
|
||||
import { Format } from "../../src/format"
|
||||
import { provideTmpdirInstance, provideTmpdirServer } from "../fixture/fixture"
|
||||
@@ -371,6 +374,44 @@ it.live("loop calls LLM and returns assistant message", () =>
|
||||
),
|
||||
)
|
||||
|
||||
it.live("prompt emits v2 prompted and synthetic events", () =>
|
||||
provideTmpdirServer(
|
||||
Effect.fnUntraced(function* () {
|
||||
const prompt = yield* SessionPrompt.Service
|
||||
const sessions = yield* Session.Service
|
||||
const chat = yield* sessions.create({ title: "Pinned" })
|
||||
|
||||
yield* prompt.prompt({
|
||||
sessionID: chat.id,
|
||||
agent: "build",
|
||||
noReply: true,
|
||||
parts: [
|
||||
{ type: "text", text: "hello v2" },
|
||||
{
|
||||
type: "file",
|
||||
mime: "text/plain",
|
||||
filename: "note.txt",
|
||||
url: "data:text/plain;base64,bm90ZSBjb250ZW50",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const messages = yield* SessionV2.Service.use((session) => session.messages({ sessionID: chat.id })).pipe(
|
||||
Effect.provide(SessionV2.layer),
|
||||
)
|
||||
const row = Database.use((db) =>
|
||||
db.select().from(SessionMessageTable).where(Database.eq(SessionMessageTable.session_id, chat.id)).get(),
|
||||
)
|
||||
expect(messages).toHaveLength(3)
|
||||
expect(messages[0]).toMatchObject({ type: "user", text: "hello v2" })
|
||||
expect(typeof row?.data.time.created).toBe("number")
|
||||
expect(messages[1]).toMatchObject({ type: "synthetic", text: expect.stringContaining("Called the Read tool") })
|
||||
expect(messages[2]).toMatchObject({ type: "synthetic", text: "note content" })
|
||||
}),
|
||||
{ git: true, config: providerCfg },
|
||||
),
|
||||
)
|
||||
|
||||
it.live("static loop returns assistant text through local provider", () =>
|
||||
provideTmpdirServer(
|
||||
Effect.fnUntraced(function* ({ llm }) {
|
||||
|
||||
@@ -1,916 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import * as DateTime from "effect/DateTime"
|
||||
import * as FastCheck from "effect/testing/FastCheck"
|
||||
import { SessionEntry } from "../../src/v2/session-entry"
|
||||
import { SessionEntryStepper } from "../../src/v2/session-entry-stepper"
|
||||
import { SessionEvent } from "../../src/v2/session-event"
|
||||
|
||||
const time = (n: number) => DateTime.makeUnsafe(n)
|
||||
|
||||
const word = FastCheck.string({ minLength: 1, maxLength: 8 })
|
||||
const text = FastCheck.string({ maxLength: 16 })
|
||||
const texts = FastCheck.array(text, { maxLength: 8 })
|
||||
const val = FastCheck.oneof(FastCheck.boolean(), FastCheck.integer(), FastCheck.string({ maxLength: 12 }))
|
||||
const dict = FastCheck.dictionary(word, val, { maxKeys: 4 })
|
||||
const files = FastCheck.array(
|
||||
word.map((x) => SessionEvent.FileAttachment.create({ uri: `file://${encodeURIComponent(x)}`, mime: "text/plain" })),
|
||||
{ maxLength: 2 },
|
||||
)
|
||||
|
||||
function maybe<A>(arb: FastCheck.Arbitrary<A>) {
|
||||
return FastCheck.oneof(FastCheck.constant(undefined), arb)
|
||||
}
|
||||
|
||||
function assistant() {
|
||||
return new SessionEntry.Assistant({
|
||||
id: SessionEvent.ID.create(),
|
||||
type: "assistant",
|
||||
time: { created: time(0) },
|
||||
content: [],
|
||||
retries: [],
|
||||
})
|
||||
}
|
||||
|
||||
function retryError(message: string) {
|
||||
return new SessionEvent.RetryError({
|
||||
message,
|
||||
isRetryable: true,
|
||||
})
|
||||
}
|
||||
|
||||
function retry(attempt: number, message: string, created: number) {
|
||||
return new SessionEntry.AssistantRetry({
|
||||
attempt,
|
||||
error: retryError(message),
|
||||
time: {
|
||||
created: time(created),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function memoryState() {
|
||||
const state: SessionEntryStepper.MemoryState = {
|
||||
entries: [],
|
||||
pending: [],
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
function active() {
|
||||
const state: SessionEntryStepper.MemoryState = {
|
||||
entries: [assistant()],
|
||||
pending: [],
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
function run(events: SessionEvent.Event[], state = memoryState()) {
|
||||
return events.reduce<SessionEntryStepper.MemoryState>((state, event) => SessionEntryStepper.step(state, event), state)
|
||||
}
|
||||
|
||||
function last(state: SessionEntryStepper.MemoryState) {
|
||||
const entry = [...state.pending, ...state.entries].reverse().find((x) => x.type === "assistant")
|
||||
expect(entry?.type).toBe("assistant")
|
||||
return entry?.type === "assistant" ? entry : undefined
|
||||
}
|
||||
|
||||
function texts_of(state: SessionEntryStepper.MemoryState) {
|
||||
const entry = last(state)
|
||||
if (!entry) return []
|
||||
return entry.content.filter((x): x is SessionEntry.AssistantText => x.type === "text")
|
||||
}
|
||||
|
||||
function reasons(state: SessionEntryStepper.MemoryState) {
|
||||
const entry = last(state)
|
||||
if (!entry) return []
|
||||
return entry.content.filter((x): x is SessionEntry.AssistantReasoning => x.type === "reasoning")
|
||||
}
|
||||
|
||||
function tools(state: SessionEntryStepper.MemoryState) {
|
||||
const entry = last(state)
|
||||
if (!entry) return []
|
||||
return entry.content.filter((x): x is SessionEntry.AssistantTool => x.type === "tool")
|
||||
}
|
||||
|
||||
function tool(state: SessionEntryStepper.MemoryState, callID: string) {
|
||||
return tools(state).find((x) => x.callID === callID)
|
||||
}
|
||||
|
||||
function retriesOf(state: SessionEntryStepper.MemoryState) {
|
||||
const entry = last(state)
|
||||
if (!entry) return []
|
||||
return entry.retries ?? []
|
||||
}
|
||||
|
||||
function adapterStore() {
|
||||
return {
|
||||
committed: [] as SessionEntry.Entry[],
|
||||
deferred: [] as SessionEntry.Entry[],
|
||||
}
|
||||
}
|
||||
|
||||
function adapterFor(store: ReturnType<typeof adapterStore>): SessionEntryStepper.Adapter<typeof store> {
|
||||
const activeAssistantIndex = () =>
|
||||
store.committed.findLastIndex((entry) => entry.type === "assistant" && !entry.time.completed)
|
||||
|
||||
const getCurrentAssistant = () => {
|
||||
const index = activeAssistantIndex()
|
||||
if (index < 0) return
|
||||
const assistant = store.committed[index]
|
||||
return assistant?.type === "assistant" ? assistant : undefined
|
||||
}
|
||||
|
||||
return {
|
||||
getCurrentAssistant,
|
||||
updateAssistant(assistant) {
|
||||
const index = activeAssistantIndex()
|
||||
if (index < 0) return
|
||||
const current = store.committed[index]
|
||||
if (current?.type !== "assistant") return
|
||||
store.committed[index] = assistant
|
||||
},
|
||||
appendEntry(entry) {
|
||||
store.committed.push(entry)
|
||||
},
|
||||
appendPending(entry) {
|
||||
store.deferred.push(entry)
|
||||
},
|
||||
finish() {
|
||||
return store
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe("session-entry-stepper", () => {
|
||||
describe("stepWith", () => {
|
||||
test("reduces through a custom adapter", () => {
|
||||
const store = adapterStore()
|
||||
store.committed.push(assistant())
|
||||
|
||||
SessionEntryStepper.stepWith(adapterFor(store), SessionEvent.Prompt.create({ text: "hello", timestamp: time(1) }))
|
||||
SessionEntryStepper.stepWith(adapterFor(store), SessionEvent.Reasoning.Started.create({ timestamp: time(2) }))
|
||||
SessionEntryStepper.stepWith(
|
||||
adapterFor(store),
|
||||
SessionEvent.Reasoning.Delta.create({ delta: "thinking", timestamp: time(3) }),
|
||||
)
|
||||
SessionEntryStepper.stepWith(
|
||||
adapterFor(store),
|
||||
SessionEvent.Reasoning.Ended.create({ text: "thought", timestamp: time(4) }),
|
||||
)
|
||||
SessionEntryStepper.stepWith(adapterFor(store), SessionEvent.Text.Started.create({ timestamp: time(5) }))
|
||||
SessionEntryStepper.stepWith(
|
||||
adapterFor(store),
|
||||
SessionEvent.Text.Delta.create({ delta: "world", timestamp: time(6) }),
|
||||
)
|
||||
SessionEntryStepper.stepWith(
|
||||
adapterFor(store),
|
||||
SessionEvent.Step.Ended.create({
|
||||
reason: "stop",
|
||||
cost: 1,
|
||||
tokens: {
|
||||
input: 1,
|
||||
output: 2,
|
||||
reasoning: 3,
|
||||
cache: {
|
||||
read: 4,
|
||||
write: 5,
|
||||
},
|
||||
},
|
||||
timestamp: time(7),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(store.deferred).toHaveLength(1)
|
||||
expect(store.deferred[0]?.type).toBe("user")
|
||||
expect(store.committed).toHaveLength(1)
|
||||
expect(store.committed[0]?.type).toBe("assistant")
|
||||
if (store.committed[0]?.type !== "assistant") return
|
||||
|
||||
expect(store.committed[0].content).toEqual([
|
||||
{ type: "reasoning", text: "thought" },
|
||||
{ type: "text", text: "world" },
|
||||
])
|
||||
expect(store.committed[0].time.completed).toEqual(time(7))
|
||||
})
|
||||
|
||||
test("aggregates retry events onto the current assistant", () => {
|
||||
const store = adapterStore()
|
||||
store.committed.push(assistant())
|
||||
|
||||
SessionEntryStepper.stepWith(
|
||||
adapterFor(store),
|
||||
SessionEvent.Retried.create({
|
||||
attempt: 1,
|
||||
error: retryError("rate limited"),
|
||||
timestamp: time(1),
|
||||
}),
|
||||
)
|
||||
SessionEntryStepper.stepWith(
|
||||
adapterFor(store),
|
||||
SessionEvent.Retried.create({
|
||||
attempt: 2,
|
||||
error: retryError("provider overloaded"),
|
||||
timestamp: time(2),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(store.committed[0]?.type).toBe("assistant")
|
||||
if (store.committed[0]?.type !== "assistant") return
|
||||
|
||||
expect(store.committed[0].retries).toEqual([retry(1, "rate limited", 1), retry(2, "provider overloaded", 2)])
|
||||
})
|
||||
})
|
||||
|
||||
describe("memory", () => {
|
||||
test("tracks and replaces the current assistant", () => {
|
||||
const state = active()
|
||||
const adapter = SessionEntryStepper.memory(state)
|
||||
const current = adapter.getCurrentAssistant()
|
||||
|
||||
expect(current?.type).toBe("assistant")
|
||||
if (!current) return
|
||||
|
||||
adapter.updateAssistant(
|
||||
new SessionEntry.Assistant({
|
||||
...current,
|
||||
content: [new SessionEntry.AssistantText({ type: "text", text: "done" })],
|
||||
time: {
|
||||
...current.time,
|
||||
completed: time(1),
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
expect(adapter.getCurrentAssistant()).toBeUndefined()
|
||||
expect(state.entries[0]?.type).toBe("assistant")
|
||||
if (state.entries[0]?.type !== "assistant") return
|
||||
|
||||
expect(state.entries[0].content).toEqual([{ type: "text", text: "done" }])
|
||||
expect(state.entries[0].time.completed).toEqual(time(1))
|
||||
})
|
||||
|
||||
test("appends committed and pending entries", () => {
|
||||
const state = memoryState()
|
||||
const adapter = SessionEntryStepper.memory(state)
|
||||
const committed = SessionEntry.User.fromEvent(
|
||||
SessionEvent.Prompt.create({ text: "committed", timestamp: time(1) }),
|
||||
)
|
||||
const pending = SessionEntry.User.fromEvent(SessionEvent.Prompt.create({ text: "pending", timestamp: time(2) }))
|
||||
|
||||
adapter.appendEntry(committed)
|
||||
adapter.appendPending(pending)
|
||||
|
||||
expect(state.entries).toEqual([committed])
|
||||
expect(state.pending).toEqual([pending])
|
||||
})
|
||||
|
||||
test("stepWith through memory records reasoning", () => {
|
||||
const state = active()
|
||||
|
||||
SessionEntryStepper.stepWith(
|
||||
SessionEntryStepper.memory(state),
|
||||
SessionEvent.Reasoning.Started.create({ timestamp: time(1) }),
|
||||
)
|
||||
SessionEntryStepper.stepWith(
|
||||
SessionEntryStepper.memory(state),
|
||||
SessionEvent.Reasoning.Delta.create({ delta: "draft", timestamp: time(2) }),
|
||||
)
|
||||
SessionEntryStepper.stepWith(
|
||||
SessionEntryStepper.memory(state),
|
||||
SessionEvent.Reasoning.Ended.create({ text: "final", timestamp: time(3) }),
|
||||
)
|
||||
|
||||
expect(reasons(state)).toEqual([{ type: "reasoning", text: "final" }])
|
||||
})
|
||||
|
||||
test("stepWith through memory records retries", () => {
|
||||
const state = active()
|
||||
|
||||
SessionEntryStepper.stepWith(
|
||||
SessionEntryStepper.memory(state),
|
||||
SessionEvent.Retried.create({
|
||||
attempt: 1,
|
||||
error: retryError("rate limited"),
|
||||
timestamp: time(1),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(retriesOf(state)).toEqual([retry(1, "rate limited", 1)])
|
||||
})
|
||||
})
|
||||
|
||||
describe("step", () => {
|
||||
describe("seeded pending assistant", () => {
|
||||
test("stores prompts in entries when no assistant is pending", () => {
|
||||
FastCheck.assert(
|
||||
FastCheck.property(word, (body) => {
|
||||
const next = SessionEntryStepper.step(
|
||||
memoryState(),
|
||||
SessionEvent.Prompt.create({ text: body, timestamp: time(1) }),
|
||||
)
|
||||
expect(next.entries).toHaveLength(1)
|
||||
expect(next.entries[0]?.type).toBe("user")
|
||||
if (next.entries[0]?.type !== "user") return
|
||||
expect(next.entries[0].text).toBe(body)
|
||||
}),
|
||||
{ numRuns: 50 },
|
||||
)
|
||||
})
|
||||
|
||||
test("stores prompts in pending when an assistant is pending", () => {
|
||||
FastCheck.assert(
|
||||
FastCheck.property(word, (body) => {
|
||||
const next = SessionEntryStepper.step(
|
||||
active(),
|
||||
SessionEvent.Prompt.create({ text: body, timestamp: time(1) }),
|
||||
)
|
||||
expect(next.pending).toHaveLength(1)
|
||||
expect(next.pending[0]?.type).toBe("user")
|
||||
if (next.pending[0]?.type !== "user") return
|
||||
expect(next.pending[0].text).toBe(body)
|
||||
}),
|
||||
{ numRuns: 50 },
|
||||
)
|
||||
})
|
||||
|
||||
test("accumulates text deltas on the latest text part", () => {
|
||||
FastCheck.assert(
|
||||
FastCheck.property(texts, (parts) => {
|
||||
const next = parts.reduce(
|
||||
(state, part, i) =>
|
||||
SessionEntryStepper.step(
|
||||
state,
|
||||
SessionEvent.Text.Delta.create({ delta: part, timestamp: time(i + 2) }),
|
||||
),
|
||||
SessionEntryStepper.step(active(), SessionEvent.Text.Started.create({ timestamp: time(1) })),
|
||||
)
|
||||
|
||||
expect(texts_of(next)).toEqual([
|
||||
{
|
||||
type: "text",
|
||||
text: parts.join(""),
|
||||
},
|
||||
])
|
||||
}),
|
||||
{ numRuns: 100 },
|
||||
)
|
||||
})
|
||||
|
||||
test("routes later text deltas to the latest text segment", () => {
|
||||
FastCheck.assert(
|
||||
FastCheck.property(texts, texts, (a, b) => {
|
||||
const next = run(
|
||||
[
|
||||
SessionEvent.Text.Started.create({ timestamp: time(1) }),
|
||||
...a.map((x, i) => SessionEvent.Text.Delta.create({ delta: x, timestamp: time(i + 2) })),
|
||||
SessionEvent.Text.Started.create({ timestamp: time(a.length + 2) }),
|
||||
...b.map((x, i) => SessionEvent.Text.Delta.create({ delta: x, timestamp: time(i + a.length + 3) })),
|
||||
],
|
||||
active(),
|
||||
)
|
||||
|
||||
expect(texts_of(next)).toEqual([
|
||||
{ type: "text", text: a.join("") },
|
||||
{ type: "text", text: b.join("") },
|
||||
])
|
||||
}),
|
||||
{ numRuns: 50 },
|
||||
)
|
||||
})
|
||||
|
||||
test("reasoning.ended replaces buffered reasoning text", () => {
|
||||
FastCheck.assert(
|
||||
FastCheck.property(texts, text, (parts, end) => {
|
||||
const next = run(
|
||||
[
|
||||
SessionEvent.Reasoning.Started.create({ timestamp: time(1) }),
|
||||
...parts.map((x, i) => SessionEvent.Reasoning.Delta.create({ delta: x, timestamp: time(i + 2) })),
|
||||
SessionEvent.Reasoning.Ended.create({ text: end, timestamp: time(parts.length + 2) }),
|
||||
],
|
||||
active(),
|
||||
)
|
||||
|
||||
expect(reasons(next)).toEqual([
|
||||
{
|
||||
type: "reasoning",
|
||||
text: end,
|
||||
},
|
||||
])
|
||||
}),
|
||||
{ numRuns: 100 },
|
||||
)
|
||||
})
|
||||
|
||||
test("tool.success completes the latest running tool", () => {
|
||||
FastCheck.assert(
|
||||
FastCheck.property(
|
||||
word,
|
||||
word,
|
||||
dict,
|
||||
maybe(text),
|
||||
maybe(dict),
|
||||
maybe(files),
|
||||
texts,
|
||||
(callID, title, input, output, metadata, attachments, parts) => {
|
||||
const next = run(
|
||||
[
|
||||
SessionEvent.Tool.Input.Started.create({ callID, name: "bash", timestamp: time(1) }),
|
||||
...parts.map((x, i) =>
|
||||
SessionEvent.Tool.Input.Delta.create({ callID, delta: x, timestamp: time(i + 2) }),
|
||||
),
|
||||
SessionEvent.Tool.Called.create({
|
||||
callID,
|
||||
tool: "bash",
|
||||
input,
|
||||
provider: { executed: true },
|
||||
timestamp: time(parts.length + 2),
|
||||
}),
|
||||
SessionEvent.Tool.Success.create({
|
||||
callID,
|
||||
title,
|
||||
output,
|
||||
metadata,
|
||||
attachments,
|
||||
provider: { executed: true },
|
||||
timestamp: time(parts.length + 3),
|
||||
}),
|
||||
],
|
||||
active(),
|
||||
)
|
||||
|
||||
const match = tool(next, callID)
|
||||
expect(match?.state.status).toBe("completed")
|
||||
if (match?.state.status !== "completed") return
|
||||
|
||||
expect(match.time.ran).toEqual(time(parts.length + 2))
|
||||
expect(match.state.input).toEqual(input)
|
||||
expect(match.state.output).toBe(output ?? "")
|
||||
expect(match.state.title).toBe(title)
|
||||
expect(match.state.metadata).toEqual(metadata ?? {})
|
||||
expect(match.state.attachments).toEqual(attachments ?? [])
|
||||
},
|
||||
),
|
||||
{ numRuns: 50 },
|
||||
)
|
||||
})
|
||||
|
||||
test("tool.error completes the latest running tool with an error", () => {
|
||||
FastCheck.assert(
|
||||
FastCheck.property(word, dict, word, maybe(dict), (callID, input, error, metadata) => {
|
||||
const next = run(
|
||||
[
|
||||
SessionEvent.Tool.Input.Started.create({ callID, name: "bash", timestamp: time(1) }),
|
||||
SessionEvent.Tool.Called.create({
|
||||
callID,
|
||||
tool: "bash",
|
||||
input,
|
||||
provider: { executed: true },
|
||||
timestamp: time(2),
|
||||
}),
|
||||
SessionEvent.Tool.Error.create({
|
||||
callID,
|
||||
error,
|
||||
metadata,
|
||||
provider: { executed: true },
|
||||
timestamp: time(3),
|
||||
}),
|
||||
],
|
||||
active(),
|
||||
)
|
||||
|
||||
const match = tool(next, callID)
|
||||
expect(match?.state.status).toBe("error")
|
||||
if (match?.state.status !== "error") return
|
||||
|
||||
expect(match.time.ran).toEqual(time(2))
|
||||
expect(match.state.input).toEqual(input)
|
||||
expect(match.state.error).toBe(error)
|
||||
expect(match.state.metadata).toEqual(metadata ?? {})
|
||||
}),
|
||||
{ numRuns: 50 },
|
||||
)
|
||||
})
|
||||
|
||||
test("tool.success is ignored before tool.called promotes the tool to running", () => {
|
||||
FastCheck.assert(
|
||||
FastCheck.property(word, word, (callID, title) => {
|
||||
const next = run(
|
||||
[
|
||||
SessionEvent.Tool.Input.Started.create({ callID, name: "bash", timestamp: time(1) }),
|
||||
SessionEvent.Tool.Success.create({
|
||||
callID,
|
||||
title,
|
||||
provider: { executed: true },
|
||||
timestamp: time(2),
|
||||
}),
|
||||
],
|
||||
active(),
|
||||
)
|
||||
const match = tool(next, callID)
|
||||
expect(match?.state).toEqual({
|
||||
status: "pending",
|
||||
input: "",
|
||||
})
|
||||
}),
|
||||
{ numRuns: 50 },
|
||||
)
|
||||
})
|
||||
|
||||
test("step.ended copies completion fields onto the pending assistant", () => {
|
||||
FastCheck.assert(
|
||||
FastCheck.property(FastCheck.integer({ min: 1, max: 1000 }), (n) => {
|
||||
const event = SessionEvent.Step.Ended.create({
|
||||
reason: "stop",
|
||||
cost: 1,
|
||||
tokens: {
|
||||
input: 1,
|
||||
output: 2,
|
||||
reasoning: 3,
|
||||
cache: {
|
||||
read: 4,
|
||||
write: 5,
|
||||
},
|
||||
},
|
||||
timestamp: time(n),
|
||||
})
|
||||
const next = SessionEntryStepper.step(active(), event)
|
||||
const entry = last(next)
|
||||
expect(entry).toBeDefined()
|
||||
if (!entry) return
|
||||
|
||||
expect(entry.time.completed).toEqual(event.timestamp)
|
||||
expect(entry.cost).toBe(event.cost)
|
||||
expect(entry.tokens).toEqual(event.tokens)
|
||||
}),
|
||||
{ numRuns: 50 },
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("known reducer gaps", () => {
|
||||
test("prompt appends immutably when no assistant is pending", () => {
|
||||
FastCheck.assert(
|
||||
FastCheck.property(word, (body) => {
|
||||
const old = memoryState()
|
||||
const next = SessionEntryStepper.step(old, SessionEvent.Prompt.create({ text: body, timestamp: time(1) }))
|
||||
expect(old).not.toBe(next)
|
||||
expect(old.entries).toHaveLength(0)
|
||||
expect(next.entries).toHaveLength(1)
|
||||
}),
|
||||
{ numRuns: 50 },
|
||||
)
|
||||
})
|
||||
|
||||
test("prompt appends immutably when an assistant is pending", () => {
|
||||
FastCheck.assert(
|
||||
FastCheck.property(word, (body) => {
|
||||
const old = active()
|
||||
const next = SessionEntryStepper.step(old, SessionEvent.Prompt.create({ text: body, timestamp: time(1) }))
|
||||
expect(old).not.toBe(next)
|
||||
expect(old.pending).toHaveLength(0)
|
||||
expect(next.pending).toHaveLength(1)
|
||||
}),
|
||||
{ numRuns: 50 },
|
||||
)
|
||||
})
|
||||
|
||||
test("step.started creates an assistant consumed by follow-up events", () => {
|
||||
FastCheck.assert(
|
||||
FastCheck.property(texts, (parts) => {
|
||||
const next = run([
|
||||
SessionEvent.Step.Started.create({
|
||||
model: {
|
||||
id: "model",
|
||||
providerID: "provider",
|
||||
},
|
||||
timestamp: time(1),
|
||||
}),
|
||||
SessionEvent.Text.Started.create({ timestamp: time(2) }),
|
||||
...parts.map((x, i) => SessionEvent.Text.Delta.create({ delta: x, timestamp: time(i + 3) })),
|
||||
SessionEvent.Step.Ended.create({
|
||||
reason: "stop",
|
||||
cost: 1,
|
||||
tokens: {
|
||||
input: 1,
|
||||
output: 2,
|
||||
reasoning: 3,
|
||||
cache: {
|
||||
read: 4,
|
||||
write: 5,
|
||||
},
|
||||
},
|
||||
timestamp: time(parts.length + 3),
|
||||
}),
|
||||
])
|
||||
const entry = last(next)
|
||||
|
||||
expect(entry).toBeDefined()
|
||||
if (!entry) return
|
||||
|
||||
expect(entry.content).toEqual([
|
||||
{
|
||||
type: "text",
|
||||
text: parts.join(""),
|
||||
},
|
||||
])
|
||||
expect(entry.time.completed).toEqual(time(parts.length + 3))
|
||||
}),
|
||||
{ numRuns: 100 },
|
||||
)
|
||||
})
|
||||
|
||||
test("replays prompt -> step -> text -> step.ended", () => {
|
||||
FastCheck.assert(
|
||||
FastCheck.property(word, texts, (body, parts) => {
|
||||
const next = run([
|
||||
SessionEvent.Prompt.create({ text: body, timestamp: time(0) }),
|
||||
SessionEvent.Step.Started.create({
|
||||
model: {
|
||||
id: "model",
|
||||
providerID: "provider",
|
||||
},
|
||||
timestamp: time(1),
|
||||
}),
|
||||
SessionEvent.Text.Started.create({ timestamp: time(2) }),
|
||||
...parts.map((x, i) => SessionEvent.Text.Delta.create({ delta: x, timestamp: time(i + 3) })),
|
||||
SessionEvent.Step.Ended.create({
|
||||
reason: "stop",
|
||||
cost: 1,
|
||||
tokens: {
|
||||
input: 1,
|
||||
output: 2,
|
||||
reasoning: 3,
|
||||
cache: {
|
||||
read: 4,
|
||||
write: 5,
|
||||
},
|
||||
},
|
||||
timestamp: time(parts.length + 3),
|
||||
}),
|
||||
])
|
||||
|
||||
expect(next.entries).toHaveLength(2)
|
||||
expect(next.entries[0]?.type).toBe("user")
|
||||
expect(next.entries[1]?.type).toBe("assistant")
|
||||
if (next.entries[1]?.type !== "assistant") return
|
||||
|
||||
expect(next.entries[1].content).toEqual([
|
||||
{
|
||||
type: "text",
|
||||
text: parts.join(""),
|
||||
},
|
||||
])
|
||||
expect(next.entries[1].time.completed).toEqual(time(parts.length + 3))
|
||||
}),
|
||||
{ numRuns: 50 },
|
||||
)
|
||||
})
|
||||
|
||||
test("replays prompt -> step -> reasoning -> tool -> success -> step.ended", () => {
|
||||
FastCheck.assert(
|
||||
FastCheck.property(
|
||||
word,
|
||||
texts,
|
||||
text,
|
||||
dict,
|
||||
word,
|
||||
maybe(text),
|
||||
maybe(dict),
|
||||
maybe(files),
|
||||
(body, reason, end, input, title, output, metadata, attachments) => {
|
||||
const callID = "call"
|
||||
const next = run([
|
||||
SessionEvent.Prompt.create({ text: body, timestamp: time(0) }),
|
||||
SessionEvent.Step.Started.create({
|
||||
model: {
|
||||
id: "model",
|
||||
providerID: "provider",
|
||||
},
|
||||
timestamp: time(1),
|
||||
}),
|
||||
SessionEvent.Reasoning.Started.create({ timestamp: time(2) }),
|
||||
...reason.map((x, i) => SessionEvent.Reasoning.Delta.create({ delta: x, timestamp: time(i + 3) })),
|
||||
SessionEvent.Reasoning.Ended.create({ text: end, timestamp: time(reason.length + 3) }),
|
||||
SessionEvent.Tool.Input.Started.create({ callID, name: "bash", timestamp: time(reason.length + 4) }),
|
||||
SessionEvent.Tool.Called.create({
|
||||
callID,
|
||||
tool: "bash",
|
||||
input,
|
||||
provider: { executed: true },
|
||||
timestamp: time(reason.length + 5),
|
||||
}),
|
||||
SessionEvent.Tool.Success.create({
|
||||
callID,
|
||||
title,
|
||||
output,
|
||||
metadata,
|
||||
attachments,
|
||||
provider: { executed: true },
|
||||
timestamp: time(reason.length + 6),
|
||||
}),
|
||||
SessionEvent.Step.Ended.create({
|
||||
reason: "stop",
|
||||
cost: 1,
|
||||
tokens: {
|
||||
input: 1,
|
||||
output: 2,
|
||||
reasoning: 3,
|
||||
cache: {
|
||||
read: 4,
|
||||
write: 5,
|
||||
},
|
||||
},
|
||||
timestamp: time(reason.length + 7),
|
||||
}),
|
||||
])
|
||||
|
||||
expect(next.entries.at(-1)?.type).toBe("assistant")
|
||||
const entry = next.entries.at(-1)
|
||||
if (entry?.type !== "assistant") return
|
||||
|
||||
expect(entry.content).toHaveLength(2)
|
||||
expect(entry.content[0]).toEqual({
|
||||
type: "reasoning",
|
||||
text: end,
|
||||
})
|
||||
expect(entry.content[1]?.type).toBe("tool")
|
||||
if (entry.content[1]?.type !== "tool") return
|
||||
expect(entry.content[1].state.status).toBe("completed")
|
||||
expect(entry.time.completed).toEqual(time(reason.length + 7))
|
||||
},
|
||||
),
|
||||
{ numRuns: 50 },
|
||||
)
|
||||
})
|
||||
|
||||
test("starting a new step completes the old assistant and appends a new active assistant", () => {
|
||||
const next = run(
|
||||
[
|
||||
SessionEvent.Step.Started.create({
|
||||
model: {
|
||||
id: "model",
|
||||
providerID: "provider",
|
||||
},
|
||||
timestamp: time(1),
|
||||
}),
|
||||
],
|
||||
active(),
|
||||
)
|
||||
expect(next.entries).toHaveLength(2)
|
||||
expect(next.entries[0]?.type).toBe("assistant")
|
||||
expect(next.entries[1]?.type).toBe("assistant")
|
||||
if (next.entries[0]?.type !== "assistant" || next.entries[1]?.type !== "assistant") return
|
||||
|
||||
expect(next.entries[0].time.completed).toEqual(time(1))
|
||||
expect(next.entries[1].time.created).toEqual(time(1))
|
||||
expect(next.entries[1].time.completed).toBeUndefined()
|
||||
})
|
||||
|
||||
test("handles sequential tools independently", () => {
|
||||
FastCheck.assert(
|
||||
FastCheck.property(dict, dict, word, word, (a, b, title, error) => {
|
||||
const next = run(
|
||||
[
|
||||
SessionEvent.Tool.Input.Started.create({ callID: "a", name: "bash", timestamp: time(1) }),
|
||||
SessionEvent.Tool.Called.create({
|
||||
callID: "a",
|
||||
tool: "bash",
|
||||
input: a,
|
||||
provider: { executed: true },
|
||||
timestamp: time(2),
|
||||
}),
|
||||
SessionEvent.Tool.Success.create({
|
||||
callID: "a",
|
||||
title,
|
||||
output: "done",
|
||||
provider: { executed: true },
|
||||
timestamp: time(3),
|
||||
}),
|
||||
SessionEvent.Tool.Input.Started.create({ callID: "b", name: "grep", timestamp: time(4) }),
|
||||
SessionEvent.Tool.Called.create({
|
||||
callID: "b",
|
||||
tool: "bash",
|
||||
input: b,
|
||||
provider: { executed: true },
|
||||
timestamp: time(5),
|
||||
}),
|
||||
SessionEvent.Tool.Error.create({
|
||||
callID: "b",
|
||||
error,
|
||||
provider: { executed: true },
|
||||
timestamp: time(6),
|
||||
}),
|
||||
],
|
||||
active(),
|
||||
)
|
||||
|
||||
const first = tool(next, "a")
|
||||
const second = tool(next, "b")
|
||||
|
||||
expect(first?.state.status).toBe("completed")
|
||||
if (first?.state.status !== "completed") return
|
||||
expect(first.state.input).toEqual(a)
|
||||
expect(first.state.output).toBe("done")
|
||||
expect(first.state.title).toBe(title)
|
||||
|
||||
expect(second?.state.status).toBe("error")
|
||||
if (second?.state.status !== "error") return
|
||||
expect(second.state.input).toEqual(b)
|
||||
expect(second.state.error).toBe(error)
|
||||
}),
|
||||
{ numRuns: 50 },
|
||||
)
|
||||
})
|
||||
|
||||
test("routes tool events by callID when tool streams interleave", () => {
|
||||
FastCheck.assert(
|
||||
FastCheck.property(dict, dict, word, word, text, text, (a, b, titleA, titleB, deltaA, deltaB) => {
|
||||
const next = run(
|
||||
[
|
||||
SessionEvent.Tool.Input.Started.create({ callID: "a", name: "bash", timestamp: time(1) }),
|
||||
SessionEvent.Tool.Input.Started.create({ callID: "b", name: "grep", timestamp: time(2) }),
|
||||
SessionEvent.Tool.Input.Delta.create({ callID: "a", delta: deltaA, timestamp: time(3) }),
|
||||
SessionEvent.Tool.Input.Delta.create({ callID: "b", delta: deltaB, timestamp: time(4) }),
|
||||
SessionEvent.Tool.Called.create({
|
||||
callID: "a",
|
||||
tool: "bash",
|
||||
input: a,
|
||||
provider: { executed: true },
|
||||
timestamp: time(5),
|
||||
}),
|
||||
SessionEvent.Tool.Called.create({
|
||||
callID: "b",
|
||||
tool: "grep",
|
||||
input: b,
|
||||
provider: { executed: true },
|
||||
timestamp: time(6),
|
||||
}),
|
||||
SessionEvent.Tool.Success.create({
|
||||
callID: "a",
|
||||
title: titleA,
|
||||
output: "done-a",
|
||||
provider: { executed: true },
|
||||
timestamp: time(7),
|
||||
}),
|
||||
SessionEvent.Tool.Success.create({
|
||||
callID: "b",
|
||||
title: titleB,
|
||||
output: "done-b",
|
||||
provider: { executed: true },
|
||||
timestamp: time(8),
|
||||
}),
|
||||
],
|
||||
active(),
|
||||
)
|
||||
|
||||
const first = tool(next, "a")
|
||||
const second = tool(next, "b")
|
||||
|
||||
expect(first?.state.status).toBe("completed")
|
||||
expect(second?.state.status).toBe("completed")
|
||||
if (first?.state.status !== "completed" || second?.state.status !== "completed") return
|
||||
|
||||
expect(first.state.input).toEqual(a)
|
||||
expect(second.state.input).toEqual(b)
|
||||
expect(first.state.title).toBe(titleA)
|
||||
expect(second.state.title).toBe(titleB)
|
||||
}),
|
||||
{ numRuns: 50 },
|
||||
)
|
||||
})
|
||||
|
||||
test("records synthetic events", () => {
|
||||
FastCheck.assert(
|
||||
FastCheck.property(word, (body) => {
|
||||
const next = SessionEntryStepper.step(
|
||||
memoryState(),
|
||||
SessionEvent.Synthetic.create({ text: body, timestamp: time(1) }),
|
||||
)
|
||||
expect(next.entries).toHaveLength(1)
|
||||
expect(next.entries[0]?.type).toBe("synthetic")
|
||||
if (next.entries[0]?.type !== "synthetic") return
|
||||
expect(next.entries[0].text).toBe(body)
|
||||
}),
|
||||
{ numRuns: 50 },
|
||||
)
|
||||
})
|
||||
|
||||
test("records compaction events", () => {
|
||||
FastCheck.assert(
|
||||
FastCheck.property(FastCheck.boolean(), maybe(FastCheck.boolean()), (auto, overflow) => {
|
||||
const next = SessionEntryStepper.step(
|
||||
memoryState(),
|
||||
SessionEvent.Compacted.create({ auto, overflow, timestamp: time(1) }),
|
||||
)
|
||||
expect(next.entries).toHaveLength(1)
|
||||
expect(next.entries[0]?.type).toBe("compaction")
|
||||
if (next.entries[0]?.type !== "compaction") return
|
||||
expect(next.entries[0].auto).toBe(auto)
|
||||
expect(next.entries[0].overflow).toBe(overflow)
|
||||
}),
|
||||
{ numRuns: 50 },
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,203 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import * as DateTime from "effect/DateTime"
|
||||
import { SessionID } from "../../src/session/schema"
|
||||
import { Event } from "../../src/v2/event"
|
||||
import { SessionEvent } from "../../src/v2/session-event"
|
||||
import { SessionMessageUpdater } from "../../src/v2/session-message-updater"
|
||||
|
||||
test("step snapshots carry over to assistant messages", () => {
|
||||
const state: SessionMessageUpdater.MemoryState = { messages: [] }
|
||||
const sessionID = SessionID.make("session")
|
||||
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: Event.ID.create(),
|
||||
type: "session.next.step.started",
|
||||
data: {
|
||||
sessionID,
|
||||
timestamp: DateTime.makeUnsafe(1),
|
||||
agent: "build",
|
||||
model: { id: "model", providerID: "provider" },
|
||||
snapshot: "before",
|
||||
},
|
||||
} satisfies SessionEvent.Event)
|
||||
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: Event.ID.create(),
|
||||
type: "session.next.step.ended",
|
||||
data: {
|
||||
sessionID,
|
||||
timestamp: DateTime.makeUnsafe(2),
|
||||
finish: "stop",
|
||||
cost: 0,
|
||||
tokens: {
|
||||
input: 1,
|
||||
output: 2,
|
||||
reasoning: 0,
|
||||
cache: { read: 0, write: 0 },
|
||||
},
|
||||
snapshot: "after",
|
||||
},
|
||||
} satisfies SessionEvent.Event)
|
||||
|
||||
expect(state.messages[0]?.type).toBe("assistant")
|
||||
if (state.messages[0]?.type !== "assistant") return
|
||||
expect(state.messages[0].snapshot).toEqual({ start: "before", end: "after" })
|
||||
expect(state.messages[0].finish).toBe("stop")
|
||||
})
|
||||
|
||||
test("text ended populates assistant text content", () => {
|
||||
const state: SessionMessageUpdater.MemoryState = { messages: [] }
|
||||
const sessionID = SessionID.make("session")
|
||||
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: Event.ID.create(),
|
||||
type: "session.next.step.started",
|
||||
data: {
|
||||
sessionID,
|
||||
timestamp: DateTime.makeUnsafe(1),
|
||||
agent: "build",
|
||||
model: { id: "model", providerID: "provider" },
|
||||
},
|
||||
} satisfies SessionEvent.Event)
|
||||
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: Event.ID.create(),
|
||||
type: "session.next.text.started",
|
||||
data: {
|
||||
sessionID,
|
||||
timestamp: DateTime.makeUnsafe(2),
|
||||
},
|
||||
} satisfies SessionEvent.Event)
|
||||
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: Event.ID.create(),
|
||||
type: "session.next.text.ended",
|
||||
data: {
|
||||
sessionID,
|
||||
timestamp: DateTime.makeUnsafe(3),
|
||||
text: "hello assistant",
|
||||
},
|
||||
} satisfies SessionEvent.Event)
|
||||
|
||||
expect(state.messages[0]?.type).toBe("assistant")
|
||||
if (state.messages[0]?.type !== "assistant") return
|
||||
expect(state.messages[0].content).toEqual([{ type: "text", text: "hello assistant" }])
|
||||
})
|
||||
|
||||
test("tool completion stores completed timestamp", () => {
|
||||
const state: SessionMessageUpdater.MemoryState = { messages: [] }
|
||||
const sessionID = SessionID.make("session")
|
||||
const callID = "call"
|
||||
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: Event.ID.create(),
|
||||
type: "session.next.step.started",
|
||||
data: {
|
||||
sessionID,
|
||||
timestamp: DateTime.makeUnsafe(1),
|
||||
agent: "build",
|
||||
model: { id: "model", providerID: "provider" },
|
||||
},
|
||||
} satisfies SessionEvent.Event)
|
||||
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: Event.ID.create(),
|
||||
type: "session.next.tool.input.started",
|
||||
data: {
|
||||
sessionID,
|
||||
timestamp: DateTime.makeUnsafe(2),
|
||||
callID,
|
||||
name: "bash",
|
||||
},
|
||||
} satisfies SessionEvent.Event)
|
||||
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: Event.ID.create(),
|
||||
type: "session.next.tool.called",
|
||||
data: {
|
||||
sessionID,
|
||||
timestamp: DateTime.makeUnsafe(3),
|
||||
callID,
|
||||
tool: "bash",
|
||||
input: { command: "pwd" },
|
||||
provider: { executed: true, metadata: { source: "provider" } },
|
||||
},
|
||||
} satisfies SessionEvent.Event)
|
||||
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: Event.ID.create(),
|
||||
type: "session.next.tool.success",
|
||||
data: {
|
||||
sessionID,
|
||||
timestamp: DateTime.makeUnsafe(4),
|
||||
callID,
|
||||
structured: {},
|
||||
content: [{ type: "text", text: "/tmp" }],
|
||||
provider: { executed: true, metadata: { status: "done" } },
|
||||
},
|
||||
} satisfies SessionEvent.Event)
|
||||
|
||||
expect(state.messages[0]?.type).toBe("assistant")
|
||||
if (state.messages[0]?.type !== "assistant") return
|
||||
expect(state.messages[0].content[0]?.type).toBe("tool")
|
||||
if (state.messages[0].content[0]?.type !== "tool") return
|
||||
expect(state.messages[0].content[0].time.completed).toEqual(DateTime.makeUnsafe(4))
|
||||
expect(state.messages[0].content[0].provider).toEqual({ executed: true, metadata: { status: "done" } })
|
||||
})
|
||||
|
||||
test("compaction events reduce to compaction message", () => {
|
||||
const state: SessionMessageUpdater.MemoryState = { messages: [] }
|
||||
const sessionID = SessionID.make("session")
|
||||
const id = Event.ID.create()
|
||||
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id,
|
||||
type: "session.next.compaction.started",
|
||||
data: {
|
||||
sessionID,
|
||||
timestamp: DateTime.makeUnsafe(1),
|
||||
reason: "auto",
|
||||
},
|
||||
} satisfies SessionEvent.Event)
|
||||
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: Event.ID.create(),
|
||||
type: "session.next.compaction.delta",
|
||||
data: {
|
||||
sessionID,
|
||||
timestamp: DateTime.makeUnsafe(2),
|
||||
text: "hello ",
|
||||
},
|
||||
} satisfies SessionEvent.Event)
|
||||
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: Event.ID.create(),
|
||||
type: "session.next.compaction.delta",
|
||||
data: {
|
||||
sessionID,
|
||||
timestamp: DateTime.makeUnsafe(3),
|
||||
text: "summary",
|
||||
},
|
||||
} satisfies SessionEvent.Event)
|
||||
|
||||
SessionMessageUpdater.update(SessionMessageUpdater.memory(state), {
|
||||
id: Event.ID.create(),
|
||||
type: "session.next.compaction.ended",
|
||||
data: {
|
||||
sessionID,
|
||||
timestamp: DateTime.makeUnsafe(4),
|
||||
text: "final summary",
|
||||
include: "recent context",
|
||||
},
|
||||
} satisfies SessionEvent.Event)
|
||||
|
||||
expect(state.messages).toHaveLength(1)
|
||||
expect(state.messages[0]).toMatchObject({
|
||||
id,
|
||||
type: "compaction",
|
||||
reason: "auto",
|
||||
summary: "final summary",
|
||||
include: "recent context",
|
||||
time: { created: DateTime.makeUnsafe(1) },
|
||||
})
|
||||
})
|
||||
@@ -187,6 +187,10 @@ import type {
|
||||
TuiSelectSessionResponses,
|
||||
TuiShowToastResponses,
|
||||
TuiSubmitPromptResponses,
|
||||
V2SessionListErrors,
|
||||
V2SessionListResponses,
|
||||
V2SessionMessagesErrors,
|
||||
V2SessionMessagesResponses,
|
||||
VcsDiffResponses,
|
||||
VcsGetResponses,
|
||||
WorktreeCreateErrors,
|
||||
@@ -1691,6 +1695,12 @@ export class Session2 extends HeyApiClient {
|
||||
workspace?: string
|
||||
parentID?: string
|
||||
title?: string
|
||||
agent?: string
|
||||
model?: {
|
||||
id: string
|
||||
providerID: string
|
||||
variant?: string
|
||||
}
|
||||
permission?: PermissionRuleset
|
||||
workspaceID?: string
|
||||
},
|
||||
@@ -1705,6 +1715,8 @@ export class Session2 extends HeyApiClient {
|
||||
{ in: "query", key: "workspace" },
|
||||
{ in: "body", key: "parentID" },
|
||||
{ in: "body", key: "title" },
|
||||
{ in: "body", key: "agent" },
|
||||
{ in: "body", key: "model" },
|
||||
{ in: "body", key: "permission" },
|
||||
{ in: "body", key: "workspaceID" },
|
||||
],
|
||||
@@ -3179,6 +3191,97 @@ export class Sync extends HeyApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
export class Session3 extends HeyApiClient {
|
||||
/**
|
||||
* List v2 sessions
|
||||
*
|
||||
* Retrieve sessions in the requested order. Items keep that order across pages; use cursor.next or cursor.previous to move through the ordered list.
|
||||
*/
|
||||
public list<ThrowOnError extends boolean = false>(
|
||||
parameters?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
limit?: number
|
||||
cursor?: string
|
||||
order?: "asc" | "desc"
|
||||
path?: string
|
||||
roots?: "true" | "false"
|
||||
start?: number
|
||||
search?: string
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
const params = buildClientParams(
|
||||
[parameters],
|
||||
[
|
||||
{
|
||||
args: [
|
||||
{ in: "query", key: "directory" },
|
||||
{ in: "query", key: "workspace" },
|
||||
{ in: "query", key: "limit" },
|
||||
{ in: "query", key: "cursor" },
|
||||
{ in: "query", key: "order" },
|
||||
{ in: "query", key: "path" },
|
||||
{ in: "query", key: "roots" },
|
||||
{ in: "query", key: "start" },
|
||||
{ in: "query", key: "search" },
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
return (options?.client ?? this.client).get<V2SessionListResponses, V2SessionListErrors, ThrowOnError>({
|
||||
url: "/api/session",
|
||||
...options,
|
||||
...params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get v2 session messages
|
||||
*
|
||||
* Retrieve projected v2 messages for a session directly from the message database.
|
||||
*/
|
||||
public messages<ThrowOnError extends boolean = false>(
|
||||
parameters: {
|
||||
sessionID: string
|
||||
directory?: string
|
||||
workspace?: string
|
||||
limit?: number
|
||||
cursor?: string
|
||||
order?: "asc" | "desc"
|
||||
},
|
||||
options?: Options<never, ThrowOnError>,
|
||||
) {
|
||||
const params = buildClientParams(
|
||||
[parameters],
|
||||
[
|
||||
{
|
||||
args: [
|
||||
{ in: "path", key: "sessionID" },
|
||||
{ in: "query", key: "directory" },
|
||||
{ in: "query", key: "workspace" },
|
||||
{ in: "query", key: "limit" },
|
||||
{ in: "query", key: "cursor" },
|
||||
{ in: "query", key: "order" },
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
return (options?.client ?? this.client).get<V2SessionMessagesResponses, V2SessionMessagesErrors, ThrowOnError>({
|
||||
url: "/api/session/{sessionID}/message",
|
||||
...options,
|
||||
...params,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class V2 extends HeyApiClient {
|
||||
private _session?: Session3
|
||||
get session(): Session3 {
|
||||
return (this._session ??= new Session3({ client: this.client }))
|
||||
}
|
||||
}
|
||||
|
||||
export class Find extends HeyApiClient {
|
||||
/**
|
||||
* Find text
|
||||
@@ -4440,6 +4543,11 @@ export class OpencodeClient extends HeyApiClient {
|
||||
return (this._sync ??= new Sync({ client: this.client }))
|
||||
}
|
||||
|
||||
private _v2?: V2
|
||||
get v2(): V2 {
|
||||
return (this._v2 ??= new V2({ client: this.client }))
|
||||
}
|
||||
|
||||
private _find?: Find
|
||||
get find(): Find {
|
||||
return (this._find ??= new Find({ client: this.client }))
|
||||
|
||||
@@ -29,11 +29,13 @@ export type Project = {
|
||||
}
|
||||
|
||||
export type EventProjectUpdated = {
|
||||
id: string
|
||||
type: "project.updated"
|
||||
properties: Project
|
||||
}
|
||||
|
||||
export type EventServerInstanceDisposed = {
|
||||
id: string
|
||||
type: "server.instance.disposed"
|
||||
properties: {
|
||||
directory: string
|
||||
@@ -41,6 +43,7 @@ export type EventServerInstanceDisposed = {
|
||||
}
|
||||
|
||||
export type EventServerConnected = {
|
||||
id: string
|
||||
type: "server.connected"
|
||||
properties: {
|
||||
[key: string]: unknown
|
||||
@@ -48,6 +51,7 @@ export type EventServerConnected = {
|
||||
}
|
||||
|
||||
export type EventGlobalDisposed = {
|
||||
id: string
|
||||
type: "global.disposed"
|
||||
properties: {
|
||||
[key: string]: unknown
|
||||
@@ -55,6 +59,7 @@ export type EventGlobalDisposed = {
|
||||
}
|
||||
|
||||
export type EventFileEdited = {
|
||||
id: string
|
||||
type: "file.edited"
|
||||
properties: {
|
||||
file: string
|
||||
@@ -62,6 +67,7 @@ export type EventFileEdited = {
|
||||
}
|
||||
|
||||
export type EventFileWatcherUpdated = {
|
||||
id: string
|
||||
type: "file.watcher.updated"
|
||||
properties: {
|
||||
file: string
|
||||
@@ -70,6 +76,7 @@ export type EventFileWatcherUpdated = {
|
||||
}
|
||||
|
||||
export type EventLspClientDiagnostics = {
|
||||
id: string
|
||||
type: "lsp.client.diagnostics"
|
||||
properties: {
|
||||
serverID: string
|
||||
@@ -78,6 +85,7 @@ export type EventLspClientDiagnostics = {
|
||||
}
|
||||
|
||||
export type EventLspUpdated = {
|
||||
id: string
|
||||
type: "lsp.updated"
|
||||
properties: {
|
||||
[key: string]: unknown
|
||||
@@ -85,6 +93,7 @@ export type EventLspUpdated = {
|
||||
}
|
||||
|
||||
export type EventMessagePartDelta = {
|
||||
id: string
|
||||
type: "message.part.delta"
|
||||
properties: {
|
||||
sessionID: string
|
||||
@@ -111,11 +120,13 @@ export type PermissionRequest = {
|
||||
}
|
||||
|
||||
export type EventPermissionAsked = {
|
||||
id: string
|
||||
type: "permission.asked"
|
||||
properties: PermissionRequest
|
||||
}
|
||||
|
||||
export type EventPermissionReplied = {
|
||||
id: string
|
||||
type: "permission.replied"
|
||||
properties: {
|
||||
sessionID: string
|
||||
@@ -133,6 +144,7 @@ export type SnapshotFileDiff = {
|
||||
}
|
||||
|
||||
export type EventSessionDiff = {
|
||||
id: string
|
||||
type: "session.diff"
|
||||
properties: {
|
||||
sessionID: string
|
||||
@@ -202,6 +214,7 @@ export type ApiError = {
|
||||
}
|
||||
|
||||
export type EventSessionError = {
|
||||
id: string
|
||||
type: "session.error"
|
||||
properties: {
|
||||
sessionID?: string
|
||||
@@ -217,6 +230,7 @@ export type EventSessionError = {
|
||||
}
|
||||
|
||||
export type EventInstallationUpdated = {
|
||||
id: string
|
||||
type: "installation.updated"
|
||||
properties: {
|
||||
version: string
|
||||
@@ -224,6 +238,7 @@ export type EventInstallationUpdated = {
|
||||
}
|
||||
|
||||
export type EventInstallationUpdateAvailable = {
|
||||
id: string
|
||||
type: "installation.update-available"
|
||||
properties: {
|
||||
version: string
|
||||
@@ -280,6 +295,7 @@ export type QuestionRequest = {
|
||||
}
|
||||
|
||||
export type EventQuestionAsked = {
|
||||
id: string
|
||||
type: "question.asked"
|
||||
properties: QuestionRequest
|
||||
}
|
||||
@@ -293,6 +309,7 @@ export type QuestionReplied = {
|
||||
}
|
||||
|
||||
export type EventQuestionReplied = {
|
||||
id: string
|
||||
type: "question.replied"
|
||||
properties: QuestionReplied
|
||||
}
|
||||
@@ -303,6 +320,7 @@ export type QuestionRejected = {
|
||||
}
|
||||
|
||||
export type EventQuestionRejected = {
|
||||
id: string
|
||||
type: "question.rejected"
|
||||
properties: QuestionRejected
|
||||
}
|
||||
@@ -323,6 +341,7 @@ export type Todo = {
|
||||
}
|
||||
|
||||
export type EventTodoUpdated = {
|
||||
id: string
|
||||
type: "todo.updated"
|
||||
properties: {
|
||||
sessionID: string
|
||||
@@ -345,6 +364,7 @@ export type SessionStatus =
|
||||
}
|
||||
|
||||
export type EventSessionStatus = {
|
||||
id: string
|
||||
type: "session.status"
|
||||
properties: {
|
||||
sessionID: string
|
||||
@@ -353,6 +373,7 @@ export type EventSessionStatus = {
|
||||
}
|
||||
|
||||
export type EventSessionIdle = {
|
||||
id: string
|
||||
type: "session.idle"
|
||||
properties: {
|
||||
sessionID: string
|
||||
@@ -360,6 +381,7 @@ export type EventSessionIdle = {
|
||||
}
|
||||
|
||||
export type EventSessionCompacted = {
|
||||
id: string
|
||||
type: "session.compacted"
|
||||
properties: {
|
||||
sessionID: string
|
||||
@@ -421,6 +443,7 @@ export type EventTuiSessionSelect = {
|
||||
}
|
||||
|
||||
export type EventMcpToolsChanged = {
|
||||
id: string
|
||||
type: "mcp.tools.changed"
|
||||
properties: {
|
||||
server: string
|
||||
@@ -428,6 +451,7 @@ export type EventMcpToolsChanged = {
|
||||
}
|
||||
|
||||
export type EventMcpBrowserOpenFailed = {
|
||||
id: string
|
||||
type: "mcp.browser.open.failed"
|
||||
properties: {
|
||||
mcpName: string
|
||||
@@ -436,6 +460,7 @@ export type EventMcpBrowserOpenFailed = {
|
||||
}
|
||||
|
||||
export type EventCommandExecuted = {
|
||||
id: string
|
||||
type: "command.executed"
|
||||
properties: {
|
||||
name: string
|
||||
@@ -446,6 +471,7 @@ export type EventCommandExecuted = {
|
||||
}
|
||||
|
||||
export type EventVcsBranchUpdated = {
|
||||
id: string
|
||||
type: "vcs.branch.updated"
|
||||
properties: {
|
||||
branch?: string
|
||||
@@ -453,6 +479,7 @@ export type EventVcsBranchUpdated = {
|
||||
}
|
||||
|
||||
export type EventWorkspaceReady = {
|
||||
id: string
|
||||
type: "workspace.ready"
|
||||
properties: {
|
||||
name: string
|
||||
@@ -460,6 +487,7 @@ export type EventWorkspaceReady = {
|
||||
}
|
||||
|
||||
export type EventWorkspaceFailed = {
|
||||
id: string
|
||||
type: "workspace.failed"
|
||||
properties: {
|
||||
message: string
|
||||
@@ -467,6 +495,7 @@ export type EventWorkspaceFailed = {
|
||||
}
|
||||
|
||||
export type EventWorkspaceRestore = {
|
||||
id: string
|
||||
type: "workspace.restore"
|
||||
properties: {
|
||||
workspaceID: string
|
||||
@@ -477,6 +506,7 @@ export type EventWorkspaceRestore = {
|
||||
}
|
||||
|
||||
export type EventWorkspaceStatus = {
|
||||
id: string
|
||||
type: "workspace.status"
|
||||
properties: {
|
||||
workspaceID: string
|
||||
@@ -485,6 +515,7 @@ export type EventWorkspaceStatus = {
|
||||
}
|
||||
|
||||
export type EventWorktreeReady = {
|
||||
id: string
|
||||
type: "worktree.ready"
|
||||
properties: {
|
||||
name: string
|
||||
@@ -493,6 +524,7 @@ export type EventWorktreeReady = {
|
||||
}
|
||||
|
||||
export type EventWorktreeFailed = {
|
||||
id: string
|
||||
type: "worktree.failed"
|
||||
properties: {
|
||||
message: string
|
||||
@@ -510,6 +542,7 @@ export type Pty = {
|
||||
}
|
||||
|
||||
export type EventPtyCreated = {
|
||||
id: string
|
||||
type: "pty.created"
|
||||
properties: {
|
||||
info: Pty
|
||||
@@ -517,6 +550,7 @@ export type EventPtyCreated = {
|
||||
}
|
||||
|
||||
export type EventPtyUpdated = {
|
||||
id: string
|
||||
type: "pty.updated"
|
||||
properties: {
|
||||
info: Pty
|
||||
@@ -524,6 +558,7 @@ export type EventPtyUpdated = {
|
||||
}
|
||||
|
||||
export type EventPtyExited = {
|
||||
id: string
|
||||
type: "pty.exited"
|
||||
properties: {
|
||||
id: string
|
||||
@@ -532,6 +567,7 @@ export type EventPtyExited = {
|
||||
}
|
||||
|
||||
export type EventPtyDeleted = {
|
||||
id: string
|
||||
type: "pty.deleted"
|
||||
properties: {
|
||||
id: string
|
||||
@@ -624,6 +660,7 @@ export type AssistantMessage = {
|
||||
export type Message = UserMessage | AssistantMessage
|
||||
|
||||
export type EventMessageUpdated = {
|
||||
id: string
|
||||
type: "message.updated"
|
||||
properties: {
|
||||
sessionID: string
|
||||
@@ -632,6 +669,7 @@ export type EventMessageUpdated = {
|
||||
}
|
||||
|
||||
export type EventMessageRemoved = {
|
||||
id: string
|
||||
type: "message.removed"
|
||||
properties: {
|
||||
sessionID: string
|
||||
@@ -903,6 +941,7 @@ export type Part =
|
||||
| CompactionPart
|
||||
|
||||
export type EventMessagePartUpdated = {
|
||||
id: string
|
||||
type: "message.part.updated"
|
||||
properties: {
|
||||
sessionID: string
|
||||
@@ -912,6 +951,7 @@ export type EventMessagePartUpdated = {
|
||||
}
|
||||
|
||||
export type EventMessagePartRemoved = {
|
||||
id: string
|
||||
type: "message.part.removed"
|
||||
properties: {
|
||||
sessionID: string
|
||||
@@ -948,6 +988,12 @@ export type Session = {
|
||||
url: string
|
||||
}
|
||||
title: string
|
||||
agent?: string
|
||||
model?: {
|
||||
id: string
|
||||
providerID: string
|
||||
variant?: string
|
||||
}
|
||||
version: string
|
||||
time: {
|
||||
created: number
|
||||
@@ -965,6 +1011,7 @@ export type Session = {
|
||||
}
|
||||
|
||||
export type EventSessionCreated = {
|
||||
id: string
|
||||
type: "session.created"
|
||||
properties: {
|
||||
sessionID: string
|
||||
@@ -973,6 +1020,7 @@ export type EventSessionCreated = {
|
||||
}
|
||||
|
||||
export type EventSessionUpdated = {
|
||||
id: string
|
||||
type: "session.updated"
|
||||
properties: {
|
||||
sessionID: string
|
||||
@@ -981,6 +1029,7 @@ export type EventSessionUpdated = {
|
||||
}
|
||||
|
||||
export type EventSessionDeleted = {
|
||||
id: string
|
||||
type: "session.deleted"
|
||||
properties: {
|
||||
sessionID: string
|
||||
@@ -988,6 +1037,345 @@ export type EventSessionDeleted = {
|
||||
}
|
||||
}
|
||||
|
||||
export type EventSessionNextAgentSwitched = {
|
||||
id: string
|
||||
type: "session.next.agent.switched"
|
||||
properties: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
agent: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventSessionNextModelSwitched = {
|
||||
id: string
|
||||
type: "session.next.model.switched"
|
||||
properties: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
id: string
|
||||
providerID: string
|
||||
variant?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type PromptSource = {
|
||||
start: number
|
||||
end: number
|
||||
text: string
|
||||
}
|
||||
|
||||
export type PromptFileAttachment = {
|
||||
uri: string
|
||||
mime: string
|
||||
name?: string
|
||||
description?: string
|
||||
source?: PromptSource
|
||||
}
|
||||
|
||||
export type PromptAgentAttachment = {
|
||||
name: string
|
||||
source?: PromptSource
|
||||
}
|
||||
|
||||
export type Prompt = {
|
||||
text: string
|
||||
files?: Array<PromptFileAttachment>
|
||||
agents?: Array<PromptAgentAttachment>
|
||||
}
|
||||
|
||||
export type EventSessionNextPrompted = {
|
||||
id: string
|
||||
type: "session.next.prompted"
|
||||
properties: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
prompt: Prompt
|
||||
}
|
||||
}
|
||||
|
||||
export type EventSessionNextSynthetic = {
|
||||
id: string
|
||||
type: "session.next.synthetic"
|
||||
properties: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
text: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventSessionNextStepStarted = {
|
||||
id: string
|
||||
type: "session.next.step.started"
|
||||
properties: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
agent: string
|
||||
model: {
|
||||
id: string
|
||||
providerID: string
|
||||
variant?: string
|
||||
}
|
||||
snapshot?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventSessionNextStepEnded = {
|
||||
id: string
|
||||
type: "session.next.step.ended"
|
||||
properties: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
finish: string
|
||||
cost: number
|
||||
tokens: {
|
||||
input: number
|
||||
output: number
|
||||
reasoning: number
|
||||
cache: {
|
||||
read: number
|
||||
write: number
|
||||
}
|
||||
}
|
||||
snapshot?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventSessionNextTextStarted = {
|
||||
id: string
|
||||
type: "session.next.text.started"
|
||||
properties: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventSessionNextTextDelta = {
|
||||
id: string
|
||||
type: "session.next.text.delta"
|
||||
properties: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
delta: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventSessionNextTextEnded = {
|
||||
id: string
|
||||
type: "session.next.text.ended"
|
||||
properties: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
text: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventSessionNextReasoningStarted = {
|
||||
id: string
|
||||
type: "session.next.reasoning.started"
|
||||
properties: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
reasoningID: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventSessionNextReasoningDelta = {
|
||||
id: string
|
||||
type: "session.next.reasoning.delta"
|
||||
properties: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
reasoningID: string
|
||||
delta: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventSessionNextReasoningEnded = {
|
||||
id: string
|
||||
type: "session.next.reasoning.ended"
|
||||
properties: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
reasoningID: string
|
||||
text: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventSessionNextToolInputStarted = {
|
||||
id: string
|
||||
type: "session.next.tool.input.started"
|
||||
properties: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
callID: string
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventSessionNextToolInputDelta = {
|
||||
id: string
|
||||
type: "session.next.tool.input.delta"
|
||||
properties: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
callID: string
|
||||
delta: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventSessionNextToolInputEnded = {
|
||||
id: string
|
||||
type: "session.next.tool.input.ended"
|
||||
properties: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
callID: string
|
||||
text: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventSessionNextToolCalled = {
|
||||
id: string
|
||||
type: "session.next.tool.called"
|
||||
properties: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
callID: string
|
||||
tool: string
|
||||
input: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
provider: {
|
||||
executed: boolean
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type ToolTextContent = {
|
||||
type: "text"
|
||||
text: string
|
||||
}
|
||||
|
||||
export type ToolFileContent = {
|
||||
type: "file"
|
||||
uri: string
|
||||
mime: string
|
||||
name?: string
|
||||
}
|
||||
|
||||
export type EventSessionNextToolProgress = {
|
||||
id: string
|
||||
type: "session.next.tool.progress"
|
||||
properties: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
callID: string
|
||||
structured: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
content: Array<ToolTextContent | ToolFileContent>
|
||||
}
|
||||
}
|
||||
|
||||
export type EventSessionNextToolSuccess = {
|
||||
id: string
|
||||
type: "session.next.tool.success"
|
||||
properties: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
callID: string
|
||||
structured: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
content: Array<ToolTextContent | ToolFileContent>
|
||||
provider: {
|
||||
executed: boolean
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type EventSessionNextToolError = {
|
||||
id: string
|
||||
type: "session.next.tool.error"
|
||||
properties: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
callID: string
|
||||
error: {
|
||||
type: string
|
||||
message: string
|
||||
}
|
||||
provider: {
|
||||
executed: boolean
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionNextRetryError = {
|
||||
message: string
|
||||
statusCode?: number
|
||||
isRetryable: boolean
|
||||
responseHeaders?: {
|
||||
[key: string]: string
|
||||
}
|
||||
responseBody?: string
|
||||
metadata?: {
|
||||
[key: string]: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventSessionNextRetried = {
|
||||
id: string
|
||||
type: "session.next.retried"
|
||||
properties: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
attempt: number
|
||||
error: SessionNextRetryError
|
||||
}
|
||||
}
|
||||
|
||||
export type EventSessionNextCompactionStarted = {
|
||||
id: string
|
||||
type: "session.next.compaction.started"
|
||||
properties: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
reason: "auto" | "manual"
|
||||
}
|
||||
}
|
||||
|
||||
export type EventSessionNextCompactionDelta = {
|
||||
id: string
|
||||
type: "session.next.compaction.delta"
|
||||
properties: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
text: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventSessionNextCompactionEnded = {
|
||||
id: string
|
||||
type: "session.next.compaction.ended"
|
||||
properties: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
text: string
|
||||
include?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SyncEventMessageUpdated = {
|
||||
type: "sync"
|
||||
name: "message.updated.1"
|
||||
@@ -1076,6 +1464,12 @@ export type SyncEventSessionUpdated = {
|
||||
url?: string | null
|
||||
}
|
||||
title?: string | null
|
||||
agent?: string | null
|
||||
model?: {
|
||||
id: string
|
||||
providerID: string
|
||||
variant?: string
|
||||
} | null
|
||||
version?: string | null
|
||||
time?: {
|
||||
created?: number | null
|
||||
@@ -1106,6 +1500,364 @@ export type SyncEventSessionDeleted = {
|
||||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionNextAgentSwitched = {
|
||||
type: "sync"
|
||||
name: "session.next.agent.switched.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
agent: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionNextModelSwitched = {
|
||||
type: "sync"
|
||||
name: "session.next.model.switched.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
id: string
|
||||
providerID: string
|
||||
variant?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionNextPrompted = {
|
||||
type: "sync"
|
||||
name: "session.next.prompted.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
prompt: Prompt
|
||||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionNextSynthetic = {
|
||||
type: "sync"
|
||||
name: "session.next.synthetic.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
text: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionNextStepStarted = {
|
||||
type: "sync"
|
||||
name: "session.next.step.started.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
agent: string
|
||||
model: {
|
||||
id: string
|
||||
providerID: string
|
||||
variant?: string
|
||||
}
|
||||
snapshot?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionNextStepEnded = {
|
||||
type: "sync"
|
||||
name: "session.next.step.ended.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
finish: string
|
||||
cost: number
|
||||
tokens: {
|
||||
input: number
|
||||
output: number
|
||||
reasoning: number
|
||||
cache: {
|
||||
read: number
|
||||
write: number
|
||||
}
|
||||
}
|
||||
snapshot?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionNextTextStarted = {
|
||||
type: "sync"
|
||||
name: "session.next.text.started.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionNextTextDelta = {
|
||||
type: "sync"
|
||||
name: "session.next.text.delta.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
delta: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionNextTextEnded = {
|
||||
type: "sync"
|
||||
name: "session.next.text.ended.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
text: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionNextReasoningStarted = {
|
||||
type: "sync"
|
||||
name: "session.next.reasoning.started.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
reasoningID: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionNextReasoningDelta = {
|
||||
type: "sync"
|
||||
name: "session.next.reasoning.delta.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
reasoningID: string
|
||||
delta: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionNextReasoningEnded = {
|
||||
type: "sync"
|
||||
name: "session.next.reasoning.ended.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
reasoningID: string
|
||||
text: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionNextToolInputStarted = {
|
||||
type: "sync"
|
||||
name: "session.next.tool.input.started.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
callID: string
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionNextToolInputDelta = {
|
||||
type: "sync"
|
||||
name: "session.next.tool.input.delta.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
callID: string
|
||||
delta: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionNextToolInputEnded = {
|
||||
type: "sync"
|
||||
name: "session.next.tool.input.ended.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
callID: string
|
||||
text: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionNextToolCalled = {
|
||||
type: "sync"
|
||||
name: "session.next.tool.called.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
callID: string
|
||||
tool: string
|
||||
input: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
provider: {
|
||||
executed: boolean
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionNextToolProgress = {
|
||||
type: "sync"
|
||||
name: "session.next.tool.progress.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
callID: string
|
||||
structured: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
content: Array<ToolTextContent | ToolFileContent>
|
||||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionNextToolSuccess = {
|
||||
type: "sync"
|
||||
name: "session.next.tool.success.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
callID: string
|
||||
structured: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
content: Array<ToolTextContent | ToolFileContent>
|
||||
provider: {
|
||||
executed: boolean
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionNextToolError = {
|
||||
type: "sync"
|
||||
name: "session.next.tool.error.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
callID: string
|
||||
error: {
|
||||
type: string
|
||||
message: string
|
||||
}
|
||||
provider: {
|
||||
executed: boolean
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionNextRetried = {
|
||||
type: "sync"
|
||||
name: "session.next.retried.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
attempt: number
|
||||
error: SessionNextRetryError
|
||||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionNextCompactionStarted = {
|
||||
type: "sync"
|
||||
name: "session.next.compaction.started.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
reason: "auto" | "manual"
|
||||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionNextCompactionDelta = {
|
||||
type: "sync"
|
||||
name: "session.next.compaction.delta.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
text: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SyncEventSessionNextCompactionEnded = {
|
||||
type: "sync"
|
||||
name: "session.next.compaction.ended.1"
|
||||
id: string
|
||||
seq: number
|
||||
aggregateID: "sessionID"
|
||||
data: {
|
||||
timestamp: number
|
||||
sessionID: string
|
||||
text: string
|
||||
include?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type GlobalEvent = {
|
||||
directory: string
|
||||
project?: string
|
||||
@@ -1158,6 +1910,29 @@ export type GlobalEvent = {
|
||||
| EventSessionCreated
|
||||
| EventSessionUpdated
|
||||
| EventSessionDeleted
|
||||
| EventSessionNextAgentSwitched
|
||||
| EventSessionNextModelSwitched
|
||||
| EventSessionNextPrompted
|
||||
| EventSessionNextSynthetic
|
||||
| EventSessionNextStepStarted
|
||||
| EventSessionNextStepEnded
|
||||
| EventSessionNextTextStarted
|
||||
| EventSessionNextTextDelta
|
||||
| EventSessionNextTextEnded
|
||||
| EventSessionNextReasoningStarted
|
||||
| EventSessionNextReasoningDelta
|
||||
| EventSessionNextReasoningEnded
|
||||
| EventSessionNextToolInputStarted
|
||||
| EventSessionNextToolInputDelta
|
||||
| EventSessionNextToolInputEnded
|
||||
| EventSessionNextToolCalled
|
||||
| EventSessionNextToolProgress
|
||||
| EventSessionNextToolSuccess
|
||||
| EventSessionNextToolError
|
||||
| EventSessionNextRetried
|
||||
| EventSessionNextCompactionStarted
|
||||
| EventSessionNextCompactionDelta
|
||||
| EventSessionNextCompactionEnded
|
||||
| SyncEventMessageUpdated
|
||||
| SyncEventMessageRemoved
|
||||
| SyncEventMessagePartUpdated
|
||||
@@ -1165,6 +1940,29 @@ export type GlobalEvent = {
|
||||
| SyncEventSessionCreated
|
||||
| SyncEventSessionUpdated
|
||||
| SyncEventSessionDeleted
|
||||
| SyncEventSessionNextAgentSwitched
|
||||
| SyncEventSessionNextModelSwitched
|
||||
| SyncEventSessionNextPrompted
|
||||
| SyncEventSessionNextSynthetic
|
||||
| SyncEventSessionNextStepStarted
|
||||
| SyncEventSessionNextStepEnded
|
||||
| SyncEventSessionNextTextStarted
|
||||
| SyncEventSessionNextTextDelta
|
||||
| SyncEventSessionNextTextEnded
|
||||
| SyncEventSessionNextReasoningStarted
|
||||
| SyncEventSessionNextReasoningDelta
|
||||
| SyncEventSessionNextReasoningEnded
|
||||
| SyncEventSessionNextToolInputStarted
|
||||
| SyncEventSessionNextToolInputDelta
|
||||
| SyncEventSessionNextToolInputEnded
|
||||
| SyncEventSessionNextToolCalled
|
||||
| SyncEventSessionNextToolProgress
|
||||
| SyncEventSessionNextToolSuccess
|
||||
| SyncEventSessionNextToolError
|
||||
| SyncEventSessionNextRetried
|
||||
| SyncEventSessionNextCompactionStarted
|
||||
| SyncEventSessionNextCompactionDelta
|
||||
| SyncEventSessionNextCompactionEnded
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1895,6 +2693,12 @@ export type GlobalSession = {
|
||||
url: string
|
||||
}
|
||||
title: string
|
||||
agent?: string
|
||||
model?: {
|
||||
id: string
|
||||
providerID: string
|
||||
variant?: string
|
||||
}
|
||||
version: string
|
||||
time: {
|
||||
created: number
|
||||
@@ -2007,6 +2811,231 @@ export type ProviderAuthAuthorization = {
|
||||
instructions: string
|
||||
}
|
||||
|
||||
export type SessionInfo = {
|
||||
id: string
|
||||
parentID?: string
|
||||
projectID: string
|
||||
workspaceID?: string
|
||||
path?: string
|
||||
agent?: string
|
||||
model?: {
|
||||
id: string
|
||||
providerID: string
|
||||
variant?: string
|
||||
}
|
||||
time: {
|
||||
created: number
|
||||
updated: number
|
||||
archived?: number
|
||||
}
|
||||
title: string
|
||||
}
|
||||
|
||||
export type V2SessionsResponse = {
|
||||
items: Array<SessionInfo>
|
||||
cursor: {
|
||||
previous?: string
|
||||
next?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionMessageAgentSwitched = {
|
||||
id: string
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
time: {
|
||||
created: number
|
||||
}
|
||||
type: "agent-switched"
|
||||
agent: string
|
||||
}
|
||||
|
||||
export type SessionMessageModelSwitched = {
|
||||
id: string
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
time: {
|
||||
created: number
|
||||
}
|
||||
type: "model-switched"
|
||||
model: {
|
||||
id: string
|
||||
providerID: string
|
||||
variant?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionMessageUser = {
|
||||
id: string
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
time: {
|
||||
created: number
|
||||
}
|
||||
text: string
|
||||
files?: Array<PromptFileAttachment>
|
||||
agents?: Array<PromptAgentAttachment>
|
||||
type: "user"
|
||||
}
|
||||
|
||||
export type SessionMessageSynthetic = {
|
||||
id: string
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
time: {
|
||||
created: number
|
||||
}
|
||||
sessionID: string
|
||||
text: string
|
||||
type: "synthetic"
|
||||
}
|
||||
|
||||
export type SessionMessageAssistantText = {
|
||||
type: "text"
|
||||
text: string
|
||||
}
|
||||
|
||||
export type SessionMessageAssistantReasoning = {
|
||||
type: "reasoning"
|
||||
id: string
|
||||
text: string
|
||||
}
|
||||
|
||||
export type SessionMessageToolStatePending = {
|
||||
status: "pending"
|
||||
input: string
|
||||
}
|
||||
|
||||
export type SessionMessageToolStateRunning = {
|
||||
status: "running"
|
||||
input: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
structured: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
content: Array<ToolTextContent | ToolFileContent>
|
||||
}
|
||||
|
||||
export type SessionMessageToolStateCompleted = {
|
||||
status: "completed"
|
||||
input: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
attachments?: Array<PromptFileAttachment>
|
||||
content: Array<ToolTextContent | ToolFileContent>
|
||||
structured: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionMessageToolStateError = {
|
||||
status: "error"
|
||||
input: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
content: Array<ToolTextContent | ToolFileContent>
|
||||
structured: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
error: {
|
||||
type: string
|
||||
message: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionMessageAssistantTool = {
|
||||
type: "tool"
|
||||
id: string
|
||||
name: string
|
||||
provider?: {
|
||||
executed: boolean
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
state:
|
||||
| SessionMessageToolStatePending
|
||||
| SessionMessageToolStateRunning
|
||||
| SessionMessageToolStateCompleted
|
||||
| SessionMessageToolStateError
|
||||
time: {
|
||||
created: number
|
||||
ran?: number
|
||||
completed?: number
|
||||
pruned?: number
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionMessageAssistant = {
|
||||
id: string
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
time: {
|
||||
created: number
|
||||
completed?: number
|
||||
}
|
||||
type: "assistant"
|
||||
agent: string
|
||||
model: {
|
||||
id: string
|
||||
providerID: string
|
||||
variant?: string
|
||||
}
|
||||
content: Array<SessionMessageAssistantText | SessionMessageAssistantReasoning | SessionMessageAssistantTool>
|
||||
snapshot?: {
|
||||
start?: string
|
||||
end?: string
|
||||
}
|
||||
finish?: string
|
||||
cost?: number
|
||||
tokens?: {
|
||||
input: number
|
||||
output: number
|
||||
reasoning: number
|
||||
cache: {
|
||||
read: number
|
||||
write: number
|
||||
}
|
||||
}
|
||||
error?: string
|
||||
}
|
||||
|
||||
export type SessionMessageCompaction = {
|
||||
type: "compaction"
|
||||
reason: "auto" | "manual"
|
||||
summary: string
|
||||
include?: string
|
||||
id: string
|
||||
metadata?: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
time: {
|
||||
created: number
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionMessage =
|
||||
| SessionMessageAgentSwitched
|
||||
| SessionMessageModelSwitched
|
||||
| SessionMessageUser
|
||||
| SessionMessageSynthetic
|
||||
| SessionMessageAssistant
|
||||
| SessionMessageCompaction
|
||||
|
||||
export type V2SessionMessagesResponse = {
|
||||
items: Array<SessionMessage>
|
||||
cursor: {
|
||||
previous?: string
|
||||
next?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type Symbol = {
|
||||
name: string
|
||||
kind: number
|
||||
@@ -2101,6 +3130,29 @@ export type Event =
|
||||
| EventSessionCreated
|
||||
| EventSessionUpdated
|
||||
| EventSessionDeleted
|
||||
| EventSessionNextAgentSwitched
|
||||
| EventSessionNextModelSwitched
|
||||
| EventSessionNextPrompted
|
||||
| EventSessionNextSynthetic
|
||||
| EventSessionNextStepStarted
|
||||
| EventSessionNextStepEnded
|
||||
| EventSessionNextTextStarted
|
||||
| EventSessionNextTextDelta
|
||||
| EventSessionNextTextEnded
|
||||
| EventSessionNextReasoningStarted
|
||||
| EventSessionNextReasoningDelta
|
||||
| EventSessionNextReasoningEnded
|
||||
| EventSessionNextToolInputStarted
|
||||
| EventSessionNextToolInputDelta
|
||||
| EventSessionNextToolInputEnded
|
||||
| EventSessionNextToolCalled
|
||||
| EventSessionNextToolProgress
|
||||
| EventSessionNextToolSuccess
|
||||
| EventSessionNextToolError
|
||||
| EventSessionNextRetried
|
||||
| EventSessionNextCompactionStarted
|
||||
| EventSessionNextCompactionDelta
|
||||
| EventSessionNextCompactionEnded
|
||||
|
||||
export type McpStatusConnected = {
|
||||
status: "connected"
|
||||
@@ -3329,6 +4381,12 @@ export type SessionCreateData = {
|
||||
body?: {
|
||||
parentID?: string
|
||||
title?: string
|
||||
agent?: string
|
||||
model?: {
|
||||
id: string
|
||||
providerID: string
|
||||
variant?: string
|
||||
}
|
||||
permission?: PermissionRuleset
|
||||
workspaceID?: string
|
||||
}
|
||||
@@ -4664,6 +5722,78 @@ export type SyncHistoryListResponses = {
|
||||
|
||||
export type SyncHistoryListResponse = SyncHistoryListResponses[keyof SyncHistoryListResponses]
|
||||
|
||||
export type V2SessionListData = {
|
||||
body?: never
|
||||
path?: never
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
limit?: number
|
||||
cursor?: string
|
||||
order?: "asc" | "desc"
|
||||
path?: string
|
||||
roots?: "true" | "false"
|
||||
start?: number
|
||||
search?: string
|
||||
}
|
||||
url: "/api/session"
|
||||
}
|
||||
|
||||
export type V2SessionListErrors = {
|
||||
/**
|
||||
* Bad request
|
||||
*/
|
||||
400: BadRequestError
|
||||
}
|
||||
|
||||
export type V2SessionListError = V2SessionListErrors[keyof V2SessionListErrors]
|
||||
|
||||
export type V2SessionListResponses = {
|
||||
/**
|
||||
* List of v2 sessions
|
||||
*/
|
||||
200: V2SessionsResponse
|
||||
}
|
||||
|
||||
export type V2SessionListResponse = V2SessionListResponses[keyof V2SessionListResponses]
|
||||
|
||||
export type V2SessionMessagesData = {
|
||||
body?: never
|
||||
path: {
|
||||
sessionID: string
|
||||
}
|
||||
query?: {
|
||||
directory?: string
|
||||
workspace?: string
|
||||
limit?: number
|
||||
cursor?: string
|
||||
order?: "asc" | "desc"
|
||||
}
|
||||
url: "/api/session/{sessionID}/message"
|
||||
}
|
||||
|
||||
export type V2SessionMessagesErrors = {
|
||||
/**
|
||||
* Bad request
|
||||
*/
|
||||
400: BadRequestError
|
||||
/**
|
||||
* Not found
|
||||
*/
|
||||
404: NotFoundError
|
||||
}
|
||||
|
||||
export type V2SessionMessagesError = V2SessionMessagesErrors[keyof V2SessionMessagesErrors]
|
||||
|
||||
export type V2SessionMessagesResponses = {
|
||||
/**
|
||||
* List of v2 session messages
|
||||
*/
|
||||
200: V2SessionMessagesResponse
|
||||
}
|
||||
|
||||
export type V2SessionMessagesResponse2 = V2SessionMessagesResponses[keyof V2SessionMessagesResponses]
|
||||
|
||||
export type FindTextData = {
|
||||
body?: never
|
||||
path?: never
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
# Session V2 Concept Gaps
|
||||
|
||||
Compared with `packages/opencode/src/session/message-v2.ts` and `packages/opencode/src/session/processor.ts`, `packages/opencode/src/v2` currently captures the rough event stream for prompts, assistant steps, text, reasoning, tools, retries, and compaction, but it does not yet capture several persisted-message and processor concepts.
|
||||
|
||||
## Message Metadata
|
||||
|
||||
- User messages are missing selected `agent`, `model`, `system`, enabled `tools`, output `format`, and summary metadata.
|
||||
- Assistant messages are missing `parentID`, `agent`, `providerID`, `modelID`, `variant`, `path.cwd`, `path.root`, deprecated `mode`, `summary`, `structured`, `finish`, and typed `error`.
|
||||
|
||||
## Output Format
|
||||
|
||||
- Text output format.
|
||||
- JSON-schema output format.
|
||||
- Structured-output retry count.
|
||||
- Structured assistant result payload.
|
||||
- Structured-output error classification.
|
||||
|
||||
## Errors
|
||||
|
||||
- Aborted error.
|
||||
- Provider auth error.
|
||||
- API error with status, retryability, headers, body, and metadata.
|
||||
- Context-overflow error.
|
||||
- Output-length error.
|
||||
- Unknown error.
|
||||
- V2 mostly reduces assistant errors to strings, except retry errors.
|
||||
|
||||
## Part Identity
|
||||
|
||||
- V1 has stable `MessageID`, `PartID`, `sessionID`, and `messageID` on every part.
|
||||
- V2 assistant content does not preserve stable per-content IDs.
|
||||
- Stable content IDs matter for deltas, updates, removals, sync events, and UI reconciliation.
|
||||
|
||||
## Part Timing And Metadata
|
||||
|
||||
- V1 text, reasoning, and tool states carry timing and provider metadata.
|
||||
- V2 assistant text and reasoning content only store text.
|
||||
- V2 events include metadata, but `SessionEntry` currently drops most provider metadata.
|
||||
|
||||
## Snapshots And Patches
|
||||
|
||||
- Snapshot parts.
|
||||
- Patch parts.
|
||||
- Step-start snapshot references.
|
||||
- Step-finish snapshot references.
|
||||
- Processor behavior that tracks a snapshot before the stream and emits patches after step finish or cleanup.
|
||||
|
||||
## Step Boundaries
|
||||
|
||||
- V1 stores `step-start` and `step-finish` as first-class parts.
|
||||
- V2 has `step.started` and `step.ended` events, but the assistant entry only stores aggregate cost and tokens.
|
||||
- V2 does not preserve step boundary parts, finish reason, or snapshot details in the entry model.
|
||||
|
||||
## Compaction
|
||||
|
||||
- V1 compaction parts have `auto`, `overflow`, and `tail_start_id`.
|
||||
- V2 compacted events have `auto` and optional `overflow`, but no retained-tail marker.
|
||||
- V1 also has history filtering semantics around completed summary messages and retained tails.
|
||||
|
||||
## Files And Sources
|
||||
|
||||
- V1 file parts have `mime`, `filename`, `url`, and typed source information.
|
||||
- V1 source variants include file, symbol, and resource sources.
|
||||
- Symbol sources include LSP range, name, and kind.
|
||||
- Resource sources include client name and URI.
|
||||
- V2 file attachments have `uri`, `mime`, `name`, `description`, and a generic text source, but lose source type, LSP metadata, and resource metadata.
|
||||
|
||||
## Agents And Subtasks
|
||||
|
||||
- Agent parts.
|
||||
- Subtask parts.
|
||||
- Subtask prompt, description, agent, model, and command.
|
||||
- V2 has agent attachments on prompts, but no assistant/session content equivalent for subtask execution.
|
||||
|
||||
## Text Flags
|
||||
|
||||
- Synthetic text flag.
|
||||
- Ignored text flag.
|
||||
- V2 has a separate synthetic entry, but no ignored text concept.
|
||||
|
||||
## Tool Calls
|
||||
|
||||
- V1 pending tool state stores parsed input and raw input text separately.
|
||||
- V2 pending tool state stores a string input but does not preserve a separate raw field.
|
||||
- V1 completed tool state has `time.start`, `time.end`, and optional `time.compacted`.
|
||||
- V2 tool time has `created`, `ran`, `completed`, and `pruned`, but the stepper currently does not set `completed` or `pruned`.
|
||||
- V1 error tool state has `time.start` and `time.end`.
|
||||
- V1 supports interrupted tool errors with `metadata.interrupted` and preserved partial output.
|
||||
- V1 tracks provider execution and provider call metadata.
|
||||
- V2 events include provider info, but `SessionEntryStepper` drops it from entries.
|
||||
- V1 has tool-output compaction and truncation behavior via `time.compacted`.
|
||||
|
||||
## Media Handling
|
||||
|
||||
- V1 models tool attachments as file parts and has provider-specific handling for media in tool results.
|
||||
- V1 can strip media, inject synthetic user messages for unsupported providers, and uses a synthetic attachment prompt.
|
||||
- V2 has attachments but not these model-message conversion semantics.
|
||||
|
||||
## Retries
|
||||
|
||||
- V1 stores retries as independently addressable retry parts.
|
||||
- V2 stores retries as an assistant aggregate.
|
||||
- V2 captures some retry information, but not the independent part identity/update model.
|
||||
|
||||
## Processor Control Flow
|
||||
|
||||
- Session status transitions: busy, retry, and idle.
|
||||
- Retry policy integration.
|
||||
- Context-overflow-driven compaction.
|
||||
- Abort and interrupt handling.
|
||||
- Permission-denied blocking.
|
||||
- Doom-loop detection.
|
||||
- Plugin hook for `experimental.text.complete`.
|
||||
- Background summary generation after steps.
|
||||
- Cleanup semantics for open text, reasoning, and tool calls.
|
||||
|
||||
## Sync And Bus Events
|
||||
|
||||
- Message updated.
|
||||
- Message removed.
|
||||
- Message part updated.
|
||||
- Message part delta.
|
||||
- Message part removed.
|
||||
- V2 has domain events, but not the sync/bus event model for persisted message and part updates/removals.
|
||||
|
||||
## History Retrieval
|
||||
|
||||
- Cursor encoding and decoding.
|
||||
- Paged message retrieval.
|
||||
- Reverse streaming through history.
|
||||
- Compaction-aware history filtering.
|
||||
Reference in New Issue
Block a user