diff --git a/packages/opencode/migration/20260331175554_session_memory/migration.sql b/packages/opencode/migration/20260331175554_session_memory/migration.sql new file mode 100644 index 000000000000..6cc3b7aebce8 --- /dev/null +++ b/packages/opencode/migration/20260331175554_session_memory/migration.sql @@ -0,0 +1,37 @@ +CREATE TABLE `memory_api_key` ( + `id` text PRIMARY KEY, + `provider` text NOT NULL, + `key_name` text NOT NULL, + `encrypted_value` text NOT NULL, + `description` text, + `time_created` integer NOT NULL, + `time_updated` integer NOT NULL +); +--> statement-breakpoint +CREATE TABLE `memory_preference` ( + `id` text PRIMARY KEY, + `key` text NOT NULL, + `value` text NOT NULL, + `type` text NOT NULL, + `description` text, + `time_created` integer NOT NULL, + `time_updated` integer NOT NULL +); +--> statement-breakpoint +CREATE TABLE `memory_rule` ( + `id` text PRIMARY KEY, + `project_id` text NOT NULL, + `pattern` text NOT NULL, + `rule` text NOT NULL, + `priority` integer DEFAULT 0 NOT NULL, + `enabled` integer DEFAULT true NOT NULL, + `time_created` integer NOT NULL, + `time_updated` integer NOT NULL, + CONSTRAINT `fk_memory_rule_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE +); +--> statement-breakpoint +CREATE INDEX `memory_api_key_provider_idx` ON `memory_api_key` (`provider`);--> statement-breakpoint +CREATE INDEX `memory_api_key_name_idx` ON `memory_api_key` (`key_name`);--> statement-breakpoint +CREATE INDEX `memory_preference_key_idx` ON `memory_preference` (`key`);--> statement-breakpoint +CREATE INDEX `memory_rule_project_idx` ON `memory_rule` (`project_id`);--> statement-breakpoint +CREATE INDEX `memory_rule_pattern_idx` ON `memory_rule` (`pattern`); \ No newline at end of file diff --git a/packages/opencode/migration/20260331175554_session_memory/snapshot.json b/packages/opencode/migration/20260331175554_session_memory/snapshot.json new file mode 100644 index 000000000000..720f24f196e0 --- /dev/null +++ b/packages/opencode/migration/20260331175554_session_memory/snapshot.json @@ -0,0 +1,1681 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "00908079-404c-4d8c-b121-5016b00a5b5b", + "prevIds": [ + "f13dfa58-7fb4-47a2-8f6b-dc70258e14ed" + ], + "ddl": [ + { + "name": "account_state", + "entityType": "tables" + }, + { + "name": "account", + "entityType": "tables" + }, + { + "name": "control_account", + "entityType": "tables" + }, + { + "name": "workspace", + "entityType": "tables" + }, + { + "name": "memory_api_key", + "entityType": "tables" + }, + { + "name": "memory_preference", + "entityType": "tables" + }, + { + "name": "memory_rule", + "entityType": "tables" + }, + { + "name": "project", + "entityType": "tables" + }, + { + "name": "message", + "entityType": "tables" + }, + { + "name": "part", + "entityType": "tables" + }, + { + "name": "permission", + "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": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "branch", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "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": "memory_api_key" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "provider", + "entityType": "columns", + "table": "memory_api_key" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "key_name", + "entityType": "columns", + "table": "memory_api_key" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "encrypted_value", + "entityType": "columns", + "table": "memory_api_key" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "description", + "entityType": "columns", + "table": "memory_api_key" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "memory_api_key" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "memory_api_key" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "memory_preference" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "key", + "entityType": "columns", + "table": "memory_preference" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "value", + "entityType": "columns", + "table": "memory_preference" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "memory_preference" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "description", + "entityType": "columns", + "table": "memory_preference" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "memory_preference" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "memory_preference" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "memory_rule" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "memory_rule" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "pattern", + "entityType": "columns", + "table": "memory_rule" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "rule", + "entityType": "columns", + "table": "memory_rule" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "priority", + "entityType": "columns", + "table": "memory_rule" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "true", + "generated": null, + "name": "enabled", + "entityType": "columns", + "table": "memory_rule" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "memory_rule" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "memory_rule" + }, + { + "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_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" + }, + { + "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": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_memory_rule_project_id_project_id_fk", + "entityType": "fks", + "table": "memory_rule" + }, + { + "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": [ + "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": "memory_api_key_pk", + "table": "memory_api_key", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "memory_preference_pk", + "table": "memory_preference", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "memory_rule_pk", + "table": "memory_rule", + "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_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": "provider", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "memory_api_key_provider_idx", + "entityType": "indexes", + "table": "memory_api_key" + }, + { + "columns": [ + { + "value": "key_name", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "memory_api_key_name_idx", + "entityType": "indexes", + "table": "memory_api_key" + }, + { + "columns": [ + { + "value": "key", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "memory_preference_key_idx", + "entityType": "indexes", + "table": "memory_preference" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "memory_rule_project_idx", + "entityType": "indexes", + "table": "memory_rule" + }, + { + "columns": [ + { + "value": "pattern", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "memory_rule_pattern_idx", + "entityType": "indexes", + "table": "memory_rule" + }, + { + "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": "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": [] +} \ No newline at end of file diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 9e56c980fbeb..4074bf2f5e06 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1074,6 +1074,13 @@ export namespace Config { .positive() .optional() .describe("Timeout in milliseconds for model context protocol (MCP) requests"), + swarm_concurrency: z + .number() + .int() + .positive() + .max(10) + .optional() + .describe("Maximum number of concurrent sub-agents to spawn in a single swarm call"), }) .optional(), }) diff --git a/packages/opencode/src/memory/index.ts b/packages/opencode/src/memory/index.ts new file mode 100644 index 000000000000..344ce5b000de --- /dev/null +++ b/packages/opencode/src/memory/index.ts @@ -0,0 +1,14 @@ +export { + MemoryID, + RuleID, + APIKeyID, + type PreferenceType, + Preference, + Rule, + APIKey, + MemoryRepoError, + MemoryServiceError, + type MemoryError, +} from "./schema" +export { MemoryRepo, type PreferenceRow, type RuleRow, type APIKeyRow } from "./repo" +export { MemoryPreferenceTable, MemoryRuleTable, MemoryAPIKeyTable } from "./memory.sql" diff --git a/packages/opencode/src/memory/memory.sql.ts b/packages/opencode/src/memory/memory.sql.ts new file mode 100644 index 000000000000..7240cf8ef7f1 --- /dev/null +++ b/packages/opencode/src/memory/memory.sql.ts @@ -0,0 +1,51 @@ +import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core" +import { Timestamps } from "../storage/schema.sql" +import { ProjectTable } from "../project/project.sql" + +export const MemoryPreferenceTable = sqliteTable( + "memory_preference", + { + id: text().primaryKey(), + key: text().notNull(), + value: text({ mode: "json" }).notNull(), + type: text().notNull(), + description: text(), + ...Timestamps, + }, + (table) => [index("memory_preference_key_idx").on(table.key)], +) + +export const MemoryRuleTable = sqliteTable( + "memory_rule", + { + id: text().primaryKey(), + project_id: text() + .notNull() + .references(() => ProjectTable.id, { onDelete: "cascade" }), + pattern: text().notNull(), + rule: text().notNull(), + priority: integer().notNull().default(0), + enabled: integer({ mode: "boolean" }).notNull().default(true), + ...Timestamps, + }, + (table) => [ + index("memory_rule_project_idx").on(table.project_id), + index("memory_rule_pattern_idx").on(table.pattern), + ], +) + +export const MemoryAPIKeyTable = sqliteTable( + "memory_api_key", + { + id: text().primaryKey(), + provider: text().notNull(), + key_name: text().notNull(), + encrypted_value: text().notNull(), + description: text(), + ...Timestamps, + }, + (table) => [ + index("memory_api_key_provider_idx").on(table.provider), + index("memory_api_key_name_idx").on(table.key_name), + ], +) diff --git a/packages/opencode/src/memory/repo.ts b/packages/opencode/src/memory/repo.ts new file mode 100644 index 000000000000..2acf72c8979c --- /dev/null +++ b/packages/opencode/src/memory/repo.ts @@ -0,0 +1,234 @@ +import { eq } from "drizzle-orm" +import { Effect, Layer, Option, Schema, ServiceMap } from "effect" + +import { Database } from "@/storage/db" +import { MemoryPreferenceTable, MemoryRuleTable, MemoryAPIKeyTable } from "./memory.sql" +import { MemoryID, RuleID, APIKeyID, Preference, Rule, APIKey, MemoryRepoError, type PreferenceType } from "./schema" + +export type PreferenceRow = (typeof MemoryPreferenceTable)["$inferSelect"] +export type RuleRow = (typeof MemoryRuleTable)["$inferSelect"] +export type APIKeyRow = (typeof MemoryAPIKeyTable)["$inferSelect"] + +type DbClient = Parameters[0] extends (db: infer T) => unknown ? T : never +type DbTransactionCallback = Parameters>[0] + +export namespace MemoryRepo { + export interface Service { + readonly getPreference: (key: string) => Effect.Effect, MemoryRepoError> + readonly getPreferences: () => Effect.Effect + readonly setPreference: (input: { + id: MemoryID + key: string + value: unknown + type: PreferenceType + description?: string + }) => Effect.Effect + readonly removePreference: (key: string) => Effect.Effect + + readonly getRulesForProject: (projectID: string) => Effect.Effect + readonly setRule: (input: { + id: RuleID + projectID: string + pattern: string + rule: string + priority?: number + enabled?: boolean + }) => Effect.Effect + readonly removeRule: (id: RuleID) => Effect.Effect + + readonly getAPIKeys: () => Effect.Effect + readonly getAPIKey: (id: APIKeyID) => Effect.Effect, MemoryRepoError> + readonly setAPIKey: (input: { + id: APIKeyID + provider: string + keyName: string + encryptedValue: string + description?: string + }) => Effect.Effect + readonly removeAPIKey: (id: APIKeyID) => Effect.Effect + } +} + +export class MemoryRepo extends ServiceMap.Service()("@opencode/MemoryRepo") { + static readonly layer: Layer.Layer = Layer.effect( + MemoryRepo, + Effect.gen(function* () { + const decodePreference = Schema.decodeUnknownSync(Preference) + const decodeRule = Schema.decodeUnknownSync(Rule) + const decodeAPIKey = Schema.decodeUnknownSync(APIKey) + + const query = (f: DbTransactionCallback) => + Effect.try({ + try: () => Database.use(f), + catch: (cause) => new MemoryRepoError({ message: "Database operation failed", cause }), + }) + + const getPreference = Effect.fn("MemoryRepo.getPreference")((key: string) => + query((db) => db.select().from(MemoryPreferenceTable).where(eq(MemoryPreferenceTable.key, key)).get()).pipe( + Effect.map((row) => (row ? Option.some(decodePreference(row)) : Option.none())), + Effect.orElseSucceed(() => Option.none()), + ), + ) + + const getPreferences = Effect.fn("MemoryRepo.getPreferences")(() => + query((db) => db.select().from(MemoryPreferenceTable).all()).pipe( + Effect.map((rows) => rows.map((row) => decodePreference(row))), + ), + ) + + const setPreference = Effect.fn("MemoryRepo.setPreference")( + (input: { id: MemoryID; key: string; value: unknown; type: PreferenceType; description?: string }) => + query((db) => { + const now = Date.now() + db.insert(MemoryPreferenceTable) + .values({ + id: input.id as string, + key: input.key, + value: input.value, + type: input.type, + description: input.description ?? null, + time_created: now, + time_updated: now, + }) + .onConflictDoUpdate({ + target: MemoryPreferenceTable.key, + set: { + value: input.value, + type: input.type, + description: input.description ?? null, + time_updated: now, + }, + }) + .run() + }).pipe(Effect.asVoid), + ) + + const removePreference = Effect.fn("MemoryRepo.removePreference")((key: string) => + query((db) => db.delete(MemoryPreferenceTable).where(eq(MemoryPreferenceTable.key, key)).run()).pipe( + Effect.asVoid, + ), + ) + + const getRulesForProject = Effect.fn("MemoryRepo.getRulesForProject")((projectID: string) => + query((db) => + db + .select() + .from(MemoryRuleTable) + .where(eq(MemoryRuleTable.project_id, projectID as string)) + .orderBy(MemoryRuleTable.priority) + .all(), + ).pipe(Effect.map((rows) => rows.map((row) => decodeRule(row)))), + ) + + const setRule = Effect.fn("MemoryRepo.setRule")( + (input: { + id: RuleID + projectID: string + pattern: string + rule: string + priority?: number + enabled?: boolean + }) => + query((db) => { + const now = Date.now() + db.insert(MemoryRuleTable) + .values({ + id: input.id as string, + project_id: input.projectID as string, + pattern: input.pattern, + rule: input.rule, + priority: input.priority ?? 0, + enabled: input.enabled ?? true, + time_created: now, + time_updated: now, + }) + .onConflictDoUpdate({ + target: MemoryRuleTable.id, + set: { + pattern: input.pattern, + rule: input.rule, + priority: input.priority ?? 0, + enabled: input.enabled ?? true, + time_updated: now, + }, + }) + .run() + }).pipe(Effect.asVoid), + ) + + const removeRule = Effect.fn("MemoryRepo.removeRule")((id: RuleID) => + query((db) => + db + .delete(MemoryRuleTable) + .where(eq(MemoryRuleTable.id, id as string)) + .run(), + ).pipe(Effect.asVoid), + ) + + const getAPIKeys = Effect.fn("MemoryRepo.getAPIKeys")(() => + query((db) => db.select().from(MemoryAPIKeyTable).all()).pipe( + Effect.map((rows) => rows.map((row) => decodeAPIKey(row))), + ), + ) + + const getAPIKey = Effect.fn("MemoryRepo.getAPIKey")((id: APIKeyID) => + query((db) => + db + .select() + .from(MemoryAPIKeyTable) + .where(eq(MemoryAPIKeyTable.id, id as string)) + .get(), + ).pipe(Effect.map((row) => (row ? Option.some(row) : Option.none()))), + ) + + const setAPIKey = Effect.fn("MemoryRepo.setAPIKey")( + (input: { id: APIKeyID; provider: string; keyName: string; encryptedValue: string; description?: string }) => + query((db) => { + const now = Date.now() + db.insert(MemoryAPIKeyTable) + .values({ + id: input.id as string, + provider: input.provider, + key_name: input.keyName, + encrypted_value: input.encryptedValue, + description: input.description ?? null, + time_created: now, + time_updated: now, + }) + .onConflictDoUpdate({ + target: MemoryAPIKeyTable.id, + set: { + encrypted_value: input.encryptedValue, + description: input.description ?? null, + time_updated: now, + }, + }) + .run() + }).pipe(Effect.asVoid), + ) + + const removeAPIKey = Effect.fn("MemoryRepo.removeAPIKey")((id: APIKeyID) => + query((db) => + db + .delete(MemoryAPIKeyTable) + .where(eq(MemoryAPIKeyTable.id, id as string)) + .run(), + ).pipe(Effect.asVoid), + ) + + return MemoryRepo.of({ + getPreference, + getPreferences, + setPreference, + removePreference, + getRulesForProject, + setRule, + removeRule, + getAPIKeys, + getAPIKey, + setAPIKey, + removeAPIKey, + }) + }), + ) +} diff --git a/packages/opencode/src/memory/schema.ts b/packages/opencode/src/memory/schema.ts new file mode 100644 index 000000000000..1f90a43c8af2 --- /dev/null +++ b/packages/opencode/src/memory/schema.ts @@ -0,0 +1,72 @@ +import { Schema } from "effect" + +import { withStatics } from "@/util/schema" + +export const MemoryID = Schema.String.pipe( + Schema.brand("MemoryID"), + withStatics((s) => ({ make: (id: string) => s.makeUnsafe(id) })), +) +export type MemoryID = Schema.Schema.Type + +export const RuleID = Schema.String.pipe( + Schema.brand("RuleID"), + withStatics((s) => ({ make: (id: string) => s.makeUnsafe(id) })), +) +export type RuleID = Schema.Schema.Type + +export const APIKeyID = Schema.String.pipe( + Schema.brand("APIKeyID"), + withStatics((s) => ({ make: (id: string) => s.makeUnsafe(id) })), +) +export type APIKeyID = Schema.Schema.Type + +export type PreferenceType = "string" | "number" | "boolean" | "json" + +const PreferenceTypeSchema = Schema.Union([ + Schema.Literal("string"), + Schema.Literal("number"), + Schema.Literal("boolean"), + Schema.Literal("json"), +]) + +export class Preference extends Schema.Class("Preference")({ + id: MemoryID, + key: Schema.String, + value: Schema.Unknown, + type: PreferenceTypeSchema, + description: Schema.NullOr(Schema.String), + time_created: Schema.Number, + time_updated: Schema.Number, +}) {} + +export class Rule extends Schema.Class("Rule")({ + id: RuleID, + project_id: Schema.String, + pattern: Schema.String, + rule: Schema.String, + priority: Schema.Number, + enabled: Schema.Boolean, + time_created: Schema.Number, + time_updated: Schema.Number, +}) {} + +export class APIKey extends Schema.Class("APIKey")({ + id: APIKeyID, + provider: Schema.String, + key_name: Schema.String, + description: Schema.NullOr(Schema.String), + time_created: Schema.Number, + time_updated: Schema.Number, +}) {} + +export class MemoryRepoError extends Schema.TaggedErrorClass()("MemoryRepoError", { + message: Schema.String, + cause: Schema.optional(Schema.Defect), +}) {} + +export class MemoryServiceError extends Schema.TaggedErrorClass()("MemoryServiceError", { + message: Schema.String, + cause: Schema.optional(Schema.Defect), +}) {} + +export type MemoryError = MemoryRepoError | MemoryServiceError diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 95be7fb4aadd..f818cbc3677d 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -34,6 +34,7 @@ import { InstanceState } from "@/effect/instance-state" import { makeRuntime } from "@/effect/run-service" import { DesktopTool } from "./desktop" import { BrowserTool } from "./browser" +import { SwarmTool } from "./swarm" export namespace ToolRegistry { const log = Log.create({ service: "tool.registry" }) @@ -133,6 +134,7 @@ export namespace ToolRegistry { CodeSearchTool, DesktopTool, BrowserTool, + SwarmTool, SkillTool, ApplyPatchTool, ...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [LspTool] : []), diff --git a/packages/opencode/src/tool/swarm.ts b/packages/opencode/src/tool/swarm.ts new file mode 100644 index 000000000000..29a6c6b84b85 --- /dev/null +++ b/packages/opencode/src/tool/swarm.ts @@ -0,0 +1,236 @@ +import { Tool } from "./tool" +import DESCRIPTION from "./swarm.txt" +import z from "zod" +import { Session } from "../session" +import { SessionID, MessageID, PartID } from "../session/schema" +import { MessageV2 } from "../session/message-v2" +import { Agent } from "../agent/agent" +import { SessionPrompt } from "../session/prompt" +import { defer } from "@/util/defer" +import { Config } from "../config/config" +import { Permission } from "@/permission" +import { errorMessage } from "../util/error" + +const parameters = z.object({ + tasks: z + .array( + z.object({ + description: z.string().describe("A short (3-5 words) description of the task"), + prompt: z.string().describe("The task for the sub-agent to perform"), + subagent_type: z.string().describe("The type of specialized agent to use for this task"), + }), + ) + .min(1, "Provide at least one task") + .max(10, "Maximum 10 tasks allowed in a single swarm call") + .describe("Array of independent tasks to execute in parallel across multiple sub-agents"), +}) + +type SwarmTask = z.infer["tasks"][number] + +type SwarmResult = + | { + success: true + description: string + output: string + sessionId: string + } + | { + success: false + description: string + error: string + } + +async function executeTask( + task: SwarmTask, + parentSessionID: SessionID, + parentMessageID: MessageID, + agentInfo: Agent.Info, + ctx: Tool.Context, +): Promise { + const config = await Config.get() + const hasTaskPermission = agentInfo.permission.some((rule) => rule.permission === "task") + const hasTodoWritePermission = agentInfo.permission.some((rule) => rule.permission === "todowrite") + + const session = await Session.create({ + parentID: parentSessionID, + title: task.description + ` (@${agentInfo.name} subagent)`, + permission: [ + ...(hasTodoWritePermission + ? [] + : [ + { + permission: "todowrite" as const, + pattern: "*" as const, + action: "deny" as const, + }, + ]), + ...(hasTaskPermission + ? [] + : [ + { + permission: "task" as const, + pattern: "*" as const, + action: "deny" as const, + }, + ]), + ...(config.experimental?.primary_tools?.map((t) => ({ + pattern: "*", + action: "allow" as const, + permission: t, + })) ?? []), + ], + }) + + const msg = await MessageV2.get({ sessionID: parentSessionID, messageID: parentMessageID }) + if (msg.info.role !== "assistant") throw new Error("Not an assistant message") + + const model = agentInfo.model ?? { + modelID: msg.info.modelID, + providerID: msg.info.providerID, + } + + const messageID = MessageID.ascending() + + function cancel() { + SessionPrompt.cancel(session.id) + } + ctx.abort.addEventListener("abort", cancel) + using _ = defer(() => ctx.abort.removeEventListener("abort", cancel)) + + const promptParts = await SessionPrompt.resolvePromptParts(task.prompt) + + const result = await SessionPrompt.prompt({ + messageID, + sessionID: session.id, + model: { + modelID: model.modelID, + providerID: model.providerID, + }, + agent: agentInfo.name, + tools: { + ...(hasTodoWritePermission ? {} : { todowrite: false }), + ...(hasTaskPermission ? {} : { task: false }), + swarm: false, + ...Object.fromEntries((config.experimental?.primary_tools ?? []).map((t) => [t, false])), + }, + parts: promptParts, + }) + + const text = result.parts.findLast((x) => x.type === "text")?.text ?? "" + + return { + success: true, + description: task.description, + output: text, + sessionId: session.id, + } +} + +export const SwarmTool = Tool.define("swarm", async (ctx) => { + const agents = await Agent.list().then((x) => x.filter((a) => a.mode !== "primary")) + + const caller = ctx?.agent + const accessibleAgents = caller + ? agents.filter((a) => Permission.evaluate("task", a.name, caller.permission).action !== "deny") + : agents + const list = accessibleAgents.toSorted((a, b) => a.name.localeCompare(b.name)) + + const description = DESCRIPTION.replace( + "{agents}", + list + .map((a) => `- ${a.name}: ${a.description ?? "This subagent should only be called manually by the user."}`) + .join("\n"), + ) + + return { + description, + parameters, + async execute(params: z.infer, ctx) { + await ctx.ask({ + permission: "swarm", + patterns: ["*"], + always: ["*"], + metadata: { + taskCount: params.tasks.length, + descriptions: params.tasks.map((t) => t.description), + }, + }) + + const config = await Config.get() + const maxConcurrency = config.experimental?.swarm_concurrency ?? 5 + + const results: SwarmResult[] = [] + const executing = new Set>() + + for (let i = 0; i < params.tasks.length; i++) { + const task = params.tasks[i] + const agent = await Agent.get(task.subagent_type) + + if (!agent) { + results.push({ + success: false, + description: task.description, + error: `Unknown agent type: ${task.subagent_type} is not a valid agent type`, + }) + continue + } + + const promise = executeTask(task, ctx.sessionID, ctx.messageID, agent, ctx).then( + (result) => { + results[i] = result + }, + (error) => { + results[i] = { + success: false, + description: task.description, + error: errorMessage(error), + } + }, + ) + + executing.add(promise) + promise.then(() => executing.delete(promise)) + + if (executing.size >= maxConcurrency) { + await Promise.race(executing) + } + } + + await Promise.all(executing) + + const successful = results.filter((r) => r.success).length + const failed = results.length - successful + + const outputParts = [ + `Swarm execution complete: ${successful}/${results.length} tasks successful${failed > 0 ? `, ${failed} failed` : ""}`, + "", + "", + ...results.map((result, idx) => { + const parts = [``] + if (result.success) { + parts.push(` ${result.sessionId}`) + parts.push(" ") + parts.push(...result.output.split("\n").map((l) => " " + l)) + parts.push(" ") + } else { + parts.push(` ${result.error}`) + } + parts.push("") + return parts.join("\n") + }), + "", + ] + + return { + title: `Swarm execution (${successful}/${results.length} successful)`, + metadata: { + total: results.length, + successful, + failed, + tasks: params.tasks.map((t) => ({ description: t.description, subagent_type: t.subagent_type })), + }, + output: outputParts.join("\n"), + } + }, + } +}) diff --git a/packages/opencode/src/tool/swarm.txt b/packages/opencode/src/tool/swarm.txt new file mode 100644 index 000000000000..a7807db51621 --- /dev/null +++ b/packages/opencode/src/tool/swarm.txt @@ -0,0 +1,35 @@ +Spawn multiple sub-agents in parallel using Bun's native workers for concurrent task execution. Each sub-agent runs independently in a separate worker thread, enabling parallel processing across multiple files, directories, or tasks. + +Use this tool when you need to: +- Execute multiple independent tasks simultaneously for faster completion +- Process multiple files or directories in parallel +- Run independent sub-agents that don't share state or have sequential dependencies +- Perform parallel research across different areas of a codebase + +IMPORTANT: Tasks should be independent with no shared state or sequential dependencies. The sub-agents run in complete isolation and cannot communicate with each other. + +Parameters: +- tasks: Array of task definitions, each with: + - description: Short 3-5 word description of the task + - prompt: Full task instructions for the sub-agent + - subagent_type: Type of specialized agent to use (e.g., "explore", "general") + +Example usage: +```json +{ + "tasks": [ + { + "description": "Find auth patterns", + "prompt": "Search the codebase for authentication middleware implementations in src/api/", + "subagent_type": "explore" + }, + { + "description": "Find error patterns", + "prompt": "Search for error handling patterns and custom Error classes", + "subagent_type": "explore" + } + ] +} +``` + +Results are returned as an array with each sub-agent's output, indexed by task order. \ No newline at end of file diff --git a/packages/opencode/test/memory/memory.test.ts b/packages/opencode/test/memory/memory.test.ts new file mode 100644 index 000000000000..f1bb0b0b6cd5 --- /dev/null +++ b/packages/opencode/test/memory/memory.test.ts @@ -0,0 +1,78 @@ +import { describe, it, expect, beforeEach } from "bun:test" +import { Effect, Option, Layer } from "effect" +import { Database } from "../../src/storage/db" +import { MemoryRepo } from "../../src/memory/repo" +import { MemoryID, RuleID, APIKeyID } from "../../src/memory/schema" + +describe("Memory", () => { + beforeEach(() => { + Database.close() + }) + + it("should set and get a preference", async () => { + const testLayer = MemoryRepo.layer + const result = await Effect.runPromise( + Effect.gen(function* () { + const repo = yield* MemoryRepo + const id = MemoryID.make("pref-1") + yield* repo.setPreference({ + id, + key: "test-key", + value: "test-value", + type: "string", + description: "Test preference", + }) + return yield* repo.getPreference("test-key") + }).pipe(Effect.provide(testLayer)), + ) + + expect(Option.isSome(result)).toBe(true) + if (Option.isSome(result)) { + const value = result.value as { key: string; value: string; type: string } + expect(value.key).toBe("test-key") + expect(value.value).toBe("test-value") + expect(value.type).toBe("string") + } + }) + + it("should set and get rules for a project", async () => { + const testLayer = MemoryRepo.layer + const result = await Effect.runPromise( + Effect.gen(function* () { + const repo = yield* MemoryRepo + const id = RuleID.make("rule-1") + yield* repo.setRule({ + id, + projectID: "test-project", + pattern: "*.ts", + rule: "Use strict TypeScript", + priority: 1, + enabled: true, + }) + return yield* repo.getRulesForProject("test-project") + }).pipe(Effect.provide(testLayer)), + ) + + expect(Array.isArray(result)).toBe(true) + }) + + it("should set and get API keys", async () => { + const testLayer = MemoryRepo.layer + const result = await Effect.runPromise( + Effect.gen(function* () { + const repo = yield* MemoryRepo + const id = APIKeyID.make("key-1") + yield* repo.setAPIKey({ + id, + provider: "openai", + keyName: "api-key-1", + encryptedValue: "encrypted-secret", + description: "Test API key", + }) + return yield* repo.getAPIKeys() + }).pipe(Effect.provide(testLayer)), + ) + + expect(Array.isArray(result)).toBe(true) + }) +})