mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add experimental agents support (#22290)
feat: add AI chat system with agent tools and chat UI Introduce the chatd subsystem and Agents UI for AI-powered chat within Coder workspaces. - Add chatd package with chat loop, message compaction, prompt management, and LLM provider integration (OpenAI, Anthropic) - Add agent tools: create workspace, list/read templates, read/write/ edit files, execute commands - Add chat API endpoints with streaming, message editing, and durable reconnection - Add database schema and migrations for chats, chat messages, chat providers, and chat model configs - Add RBAC policies and dbauthz enforcement for chat resources - Add Agents UI pages with conversation timeline, queued messages list, diff viewer, and model configuration panel - Add comprehensive test coverage including coderd integration tests, chatd unit tests, and Storybook stories - Gate feature behind experiments flag --------- Co-authored-by: Cian Johnston <cian@coder.com> Co-authored-by: Danielle Maywood <danielle@themaywoods.com> Co-authored-by: Jeremy Ruppel <jeremy@coder.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Cian Johnston
Danielle Maywood
Jeremy Ruppel
Claude Sonnet 4.6
parent
67da4e8b56
commit
edee917d88
@@ -0,0 +1,409 @@
|
||||
-- name: DeleteChatByID :exec
|
||||
DELETE FROM
|
||||
chats
|
||||
WHERE
|
||||
id = @id::uuid;
|
||||
|
||||
-- name: DeleteChatMessagesByChatID :exec
|
||||
DELETE FROM
|
||||
chat_messages
|
||||
WHERE
|
||||
chat_id = @chat_id::uuid;
|
||||
|
||||
-- name: DeleteChatMessagesAfterID :exec
|
||||
DELETE FROM
|
||||
chat_messages
|
||||
WHERE
|
||||
chat_id = @chat_id::uuid
|
||||
AND id > @after_id::bigint;
|
||||
|
||||
-- name: GetChatByID :one
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
chats
|
||||
WHERE
|
||||
id = @id::uuid;
|
||||
|
||||
-- name: GetChatMessageByID :one
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
chat_messages
|
||||
WHERE
|
||||
id = @id::bigint;
|
||||
|
||||
-- name: GetChatMessagesByChatID :many
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
chat_messages
|
||||
WHERE
|
||||
chat_id = @chat_id::uuid
|
||||
AND visibility IN ('user', 'both')
|
||||
ORDER BY
|
||||
created_at ASC;
|
||||
|
||||
-- name: GetChatMessagesForPromptByChatID :many
|
||||
WITH latest_compressed_summary AS (
|
||||
SELECT
|
||||
id
|
||||
FROM
|
||||
chat_messages
|
||||
WHERE
|
||||
chat_id = @chat_id::uuid
|
||||
AND role = 'system'
|
||||
AND visibility IN ('model', 'both')
|
||||
AND compressed = TRUE
|
||||
ORDER BY
|
||||
created_at DESC,
|
||||
id DESC
|
||||
LIMIT
|
||||
1
|
||||
)
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
chat_messages
|
||||
WHERE
|
||||
chat_id = @chat_id::uuid
|
||||
AND visibility IN ('model', 'both')
|
||||
AND (
|
||||
(
|
||||
role = 'system'
|
||||
AND compressed = FALSE
|
||||
)
|
||||
OR (
|
||||
compressed = FALSE
|
||||
AND (
|
||||
NOT EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
latest_compressed_summary
|
||||
)
|
||||
OR id > (
|
||||
SELECT
|
||||
id
|
||||
FROM
|
||||
latest_compressed_summary
|
||||
)
|
||||
)
|
||||
)
|
||||
OR id = (
|
||||
SELECT
|
||||
id
|
||||
FROM
|
||||
latest_compressed_summary
|
||||
)
|
||||
)
|
||||
ORDER BY
|
||||
created_at ASC,
|
||||
id ASC;
|
||||
|
||||
-- name: GetChatsByOwnerID :many
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
chats
|
||||
WHERE
|
||||
owner_id = @owner_id::uuid
|
||||
ORDER BY
|
||||
updated_at DESC;
|
||||
|
||||
-- name: ListChildChatsByParentID :many
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
chats
|
||||
WHERE
|
||||
parent_chat_id = @parent_chat_id::uuid
|
||||
ORDER BY
|
||||
created_at ASC;
|
||||
|
||||
-- name: ListChatsByRootID :many
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
chats
|
||||
WHERE
|
||||
root_chat_id = @root_chat_id::uuid
|
||||
ORDER BY
|
||||
created_at ASC;
|
||||
|
||||
-- name: InsertChat :one
|
||||
INSERT INTO chats (
|
||||
owner_id,
|
||||
workspace_id,
|
||||
workspace_agent_id,
|
||||
parent_chat_id,
|
||||
root_chat_id,
|
||||
last_model_config_id,
|
||||
title
|
||||
) VALUES (
|
||||
@owner_id::uuid,
|
||||
sqlc.narg('workspace_id')::uuid,
|
||||
sqlc.narg('workspace_agent_id')::uuid,
|
||||
sqlc.narg('parent_chat_id')::uuid,
|
||||
sqlc.narg('root_chat_id')::uuid,
|
||||
@last_model_config_id::uuid,
|
||||
@title::text
|
||||
)
|
||||
RETURNING
|
||||
*;
|
||||
|
||||
-- name: InsertChatMessage :one
|
||||
WITH updated_chat AS (
|
||||
UPDATE
|
||||
chats
|
||||
SET
|
||||
last_model_config_id = sqlc.narg('model_config_id')::uuid
|
||||
WHERE
|
||||
id = @chat_id::uuid
|
||||
AND sqlc.narg('model_config_id')::uuid IS NOT NULL
|
||||
)
|
||||
INSERT INTO chat_messages (
|
||||
chat_id,
|
||||
model_config_id,
|
||||
role,
|
||||
content,
|
||||
visibility,
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
total_tokens,
|
||||
reasoning_tokens,
|
||||
cache_creation_tokens,
|
||||
cache_read_tokens,
|
||||
context_limit,
|
||||
compressed
|
||||
) VALUES (
|
||||
@chat_id::uuid,
|
||||
sqlc.narg('model_config_id')::uuid,
|
||||
@role::text,
|
||||
sqlc.narg('content')::jsonb,
|
||||
@visibility::chat_message_visibility,
|
||||
sqlc.narg('input_tokens')::bigint,
|
||||
sqlc.narg('output_tokens')::bigint,
|
||||
sqlc.narg('total_tokens')::bigint,
|
||||
sqlc.narg('reasoning_tokens')::bigint,
|
||||
sqlc.narg('cache_creation_tokens')::bigint,
|
||||
sqlc.narg('cache_read_tokens')::bigint,
|
||||
sqlc.narg('context_limit')::bigint,
|
||||
COALESCE(sqlc.narg('compressed')::boolean, FALSE)
|
||||
)
|
||||
RETURNING
|
||||
*;
|
||||
|
||||
-- name: UpdateChatMessageByID :one
|
||||
UPDATE
|
||||
chat_messages
|
||||
SET
|
||||
model_config_id = COALESCE(sqlc.narg('model_config_id')::uuid, model_config_id),
|
||||
content = sqlc.narg('content')::jsonb
|
||||
WHERE
|
||||
id = @id::bigint
|
||||
RETURNING
|
||||
*;
|
||||
|
||||
-- name: UpdateChatByID :one
|
||||
UPDATE
|
||||
chats
|
||||
SET
|
||||
title = @title::text,
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
id = @id::uuid
|
||||
RETURNING
|
||||
*;
|
||||
|
||||
-- name: UpdateChatWorkspace :one
|
||||
UPDATE
|
||||
chats
|
||||
SET
|
||||
workspace_id = sqlc.narg('workspace_id')::uuid,
|
||||
workspace_agent_id = sqlc.narg('workspace_agent_id')::uuid,
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
id = @id::uuid
|
||||
RETURNING
|
||||
*;
|
||||
|
||||
-- name: AcquireChat :one
|
||||
-- Acquires a pending chat for processing. Uses SKIP LOCKED to prevent
|
||||
-- multiple replicas from acquiring the same chat.
|
||||
UPDATE
|
||||
chats
|
||||
SET
|
||||
status = 'running'::chat_status,
|
||||
started_at = @started_at::timestamptz,
|
||||
heartbeat_at = @started_at::timestamptz,
|
||||
updated_at = @started_at::timestamptz,
|
||||
worker_id = @worker_id::uuid
|
||||
WHERE
|
||||
id = (
|
||||
SELECT
|
||||
id
|
||||
FROM
|
||||
chats
|
||||
WHERE
|
||||
status = 'pending'::chat_status
|
||||
ORDER BY
|
||||
updated_at ASC
|
||||
FOR UPDATE
|
||||
SKIP LOCKED
|
||||
LIMIT
|
||||
1
|
||||
)
|
||||
RETURNING
|
||||
*;
|
||||
|
||||
-- name: UpdateChatStatus :one
|
||||
UPDATE
|
||||
chats
|
||||
SET
|
||||
status = @status::chat_status,
|
||||
worker_id = sqlc.narg('worker_id')::uuid,
|
||||
started_at = sqlc.narg('started_at')::timestamptz,
|
||||
heartbeat_at = sqlc.narg('heartbeat_at')::timestamptz,
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
id = @id::uuid
|
||||
RETURNING
|
||||
*;
|
||||
|
||||
-- name: GetStaleChats :many
|
||||
-- Find chats that appear stuck (running but heartbeat has expired).
|
||||
-- Used for recovery after coderd crashes or long hangs.
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
chats
|
||||
WHERE
|
||||
status = 'running'::chat_status
|
||||
AND heartbeat_at < @stale_threshold::timestamptz;
|
||||
|
||||
-- name: UpdateChatHeartbeat :execrows
|
||||
-- Bumps the heartbeat timestamp for a running chat so that other
|
||||
-- replicas know the worker is still alive.
|
||||
UPDATE
|
||||
chats
|
||||
SET
|
||||
heartbeat_at = NOW()
|
||||
WHERE
|
||||
id = @id::uuid
|
||||
AND worker_id = @worker_id::uuid
|
||||
AND status = 'running'::chat_status;
|
||||
|
||||
-- name: GetChatDiffStatusByChatID :one
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
chat_diff_statuses
|
||||
WHERE
|
||||
chat_id = @chat_id::uuid;
|
||||
|
||||
-- name: GetChatDiffStatusesByChatIDs :many
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
chat_diff_statuses
|
||||
WHERE
|
||||
chat_id = ANY(@chat_ids::uuid[]);
|
||||
|
||||
-- name: UpsertChatDiffStatusReference :one
|
||||
INSERT INTO chat_diff_statuses (
|
||||
chat_id,
|
||||
url,
|
||||
git_branch,
|
||||
git_remote_origin,
|
||||
stale_at
|
||||
) VALUES (
|
||||
@chat_id::uuid,
|
||||
sqlc.narg('url')::text,
|
||||
@git_branch::text,
|
||||
@git_remote_origin::text,
|
||||
@stale_at::timestamptz
|
||||
)
|
||||
ON CONFLICT (chat_id) DO UPDATE
|
||||
SET
|
||||
url = CASE
|
||||
WHEN EXCLUDED.url IS NOT NULL THEN EXCLUDED.url
|
||||
ELSE chat_diff_statuses.url
|
||||
END,
|
||||
git_branch = CASE
|
||||
WHEN EXCLUDED.git_branch != '' THEN EXCLUDED.git_branch
|
||||
ELSE chat_diff_statuses.git_branch
|
||||
END,
|
||||
git_remote_origin = CASE
|
||||
WHEN EXCLUDED.git_remote_origin != '' THEN EXCLUDED.git_remote_origin
|
||||
ELSE chat_diff_statuses.git_remote_origin
|
||||
END,
|
||||
stale_at = EXCLUDED.stale_at,
|
||||
updated_at = NOW()
|
||||
RETURNING
|
||||
*;
|
||||
|
||||
-- name: UpsertChatDiffStatus :one
|
||||
INSERT INTO chat_diff_statuses (
|
||||
chat_id,
|
||||
url,
|
||||
pull_request_state,
|
||||
changes_requested,
|
||||
additions,
|
||||
deletions,
|
||||
changed_files,
|
||||
refreshed_at,
|
||||
stale_at
|
||||
) VALUES (
|
||||
@chat_id::uuid,
|
||||
sqlc.narg('url')::text,
|
||||
sqlc.narg('pull_request_state')::text,
|
||||
@changes_requested::boolean,
|
||||
@additions::integer,
|
||||
@deletions::integer,
|
||||
@changed_files::integer,
|
||||
@refreshed_at::timestamptz,
|
||||
@stale_at::timestamptz
|
||||
)
|
||||
ON CONFLICT (chat_id) DO UPDATE
|
||||
SET
|
||||
url = EXCLUDED.url,
|
||||
pull_request_state = EXCLUDED.pull_request_state,
|
||||
changes_requested = EXCLUDED.changes_requested,
|
||||
additions = EXCLUDED.additions,
|
||||
deletions = EXCLUDED.deletions,
|
||||
changed_files = EXCLUDED.changed_files,
|
||||
refreshed_at = EXCLUDED.refreshed_at,
|
||||
stale_at = EXCLUDED.stale_at,
|
||||
updated_at = NOW()
|
||||
RETURNING
|
||||
*;
|
||||
|
||||
-- name: InsertChatQueuedMessage :one
|
||||
INSERT INTO chat_queued_messages (chat_id, content)
|
||||
VALUES (@chat_id, @content)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetChatQueuedMessages :many
|
||||
SELECT * FROM chat_queued_messages
|
||||
WHERE chat_id = @chat_id
|
||||
ORDER BY id ASC;
|
||||
|
||||
-- name: DeleteChatQueuedMessage :exec
|
||||
DELETE FROM chat_queued_messages WHERE id = @id AND chat_id = @chat_id;
|
||||
|
||||
-- name: DeleteAllChatQueuedMessages :exec
|
||||
DELETE FROM chat_queued_messages WHERE chat_id = @chat_id;
|
||||
|
||||
-- name: PopNextQueuedMessage :one
|
||||
DELETE FROM chat_queued_messages
|
||||
WHERE id = (
|
||||
SELECT cqm.id FROM chat_queued_messages cqm
|
||||
WHERE cqm.chat_id = @chat_id
|
||||
ORDER BY cqm.id ASC
|
||||
LIMIT 1
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetChatByIDForUpdate :one
|
||||
SELECT * FROM chats WHERE id = @id::uuid FOR UPDATE;
|
||||
Reference in New Issue
Block a user