mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 01:16:45 +08:00
The daemon resolves one skill bundle per request -- deliberately, so each download gets its own size-scaled deadline and is cached independently (GH #4505). ResolveTaskSkillBundles answered each of those by calling LoadAgentSkillBundles, which reads and hashes EVERY skill the agent has, then picks the single requested bundle out of the result and discards the rest. So the per-request cost scaled with the agent's skill count, and the number of requests did too. A cold dispatch for an agent with N skills read the whole skill set N+1 times and computed N*(N+1) bundle hashes to deliver N bundles. #7689 cut each of those loads from N+1 queries to 2, which fixed the round-trip count but not the multiplier: the bytes read from Postgres and the SHA-256 work stayed quadratic, and because the daemon issues these requests serially, that work lands directly in prepare latency. Load only what was asked for: - ListAgentSkillsByIDs is ListAgentSkills narrowed to a set of ids. The junction predicate is unchanged, so it is still the authorization: an id the agent does not have enabled returns no row, which the caller reports as not-found. It is deliberately NOT workspace-scoped, because ListAgentSkills is not either -- a stricter resolve could 404 a ref that claim had just advertised, breaking prepare instead of protecting it. - Files come from ListSkillFilesBySkillIDs, the batch query #7689 added, for those skills only. - Builtins resolve by name from the embedded FS and touch no query. - Only the requested skills are hashed. For 15 skills / 600KB that takes a cold dispatch from ~9.6MB read and 240 hashes to ~1.2MB and 30, with no schema change, no new index, and no change to the wire contract: stale-hash still returns the current bundle, plugin hash pinning still conflicts, response order still follows the request, and a read failure still fails closed with a 500 rather than a 404 the daemon would treat as permanent. LoadAgentSkills and the new loader share one assembly helper, so claim and resolve cannot drift on skill order, per-skill file order, or the nil file list of a skill with no files. Tests: an agent with three skills resolving one must issue one scoped query for one id and never the full ListAgentSkills; a builtin ref must touch no query at all; request order and ref dedup; and the denials the old full-set lookup used to provide -- unassigned skill, disabled assignment, another workspace's skill, unparseable id, a source with no server-side producer -- plus a scoped-read failure returning 500. Both the scoped load and the enabled predicate are mutation-verified. Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]>
762 lines
21 KiB
Go
762 lines
21 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: skill.sql
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const addAgentSkill = `-- name: AddAgentSkill :exec
|
|
INSERT INTO agent_skill (agent_id, skill_id)
|
|
VALUES ($1, $2)
|
|
ON CONFLICT DO NOTHING
|
|
`
|
|
|
|
type AddAgentSkillParams struct {
|
|
AgentID pgtype.UUID `json:"agent_id"`
|
|
SkillID pgtype.UUID `json:"skill_id"`
|
|
}
|
|
|
|
func (q *Queries) AddAgentSkill(ctx context.Context, arg AddAgentSkillParams) error {
|
|
_, err := q.db.Exec(ctx, addAgentSkill, arg.AgentID, arg.SkillID)
|
|
return err
|
|
}
|
|
|
|
const createSkill = `-- name: CreateSkill :one
|
|
INSERT INTO skill (workspace_id, name, description, content, config, created_by)
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
RETURNING id, workspace_id, name, description, content, config, created_by, created_at, updated_at, plugin_installation_id
|
|
`
|
|
|
|
type CreateSkillParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Content string `json:"content"`
|
|
Config []byte `json:"config"`
|
|
CreatedBy pgtype.UUID `json:"created_by"`
|
|
}
|
|
|
|
func (q *Queries) CreateSkill(ctx context.Context, arg CreateSkillParams) (Skill, error) {
|
|
row := q.db.QueryRow(ctx, createSkill,
|
|
arg.WorkspaceID,
|
|
arg.Name,
|
|
arg.Description,
|
|
arg.Content,
|
|
arg.Config,
|
|
arg.CreatedBy,
|
|
)
|
|
var i Skill
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.Content,
|
|
&i.Config,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.PluginInstallationID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const deleteSkill = `-- name: DeleteSkill :exec
|
|
DELETE FROM skill WHERE id = $1 AND workspace_id = $2
|
|
`
|
|
|
|
type DeleteSkillParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
// Defense-in-depth: workspace_id is a SQL-layer tenant guard. See DeleteIssue.
|
|
func (q *Queries) DeleteSkill(ctx context.Context, arg DeleteSkillParams) error {
|
|
_, err := q.db.Exec(ctx, deleteSkill, arg.ID, arg.WorkspaceID)
|
|
return err
|
|
}
|
|
|
|
const deleteSkillFile = `-- name: DeleteSkillFile :exec
|
|
DELETE FROM skill_file WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteSkillFile(ctx context.Context, id pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteSkillFile, id)
|
|
return err
|
|
}
|
|
|
|
const deleteSkillFilesBySkill = `-- name: DeleteSkillFilesBySkill :exec
|
|
DELETE FROM skill_file WHERE skill_id = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteSkillFilesBySkill(ctx context.Context, skillID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteSkillFilesBySkill, skillID)
|
|
return err
|
|
}
|
|
|
|
const getSkill = `-- name: GetSkill :one
|
|
SELECT id, workspace_id, name, description, content, config, created_by, created_at, updated_at, plugin_installation_id FROM skill
|
|
WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) GetSkill(ctx context.Context, id pgtype.UUID) (Skill, error) {
|
|
row := q.db.QueryRow(ctx, getSkill, id)
|
|
var i Skill
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.Content,
|
|
&i.Config,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.PluginInstallationID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getSkillByWorkspaceAndName = `-- name: GetSkillByWorkspaceAndName :one
|
|
SELECT id, workspace_id, name, description, content, config, created_by, created_at, updated_at, plugin_installation_id FROM skill
|
|
WHERE workspace_id = $1 AND name = $2
|
|
`
|
|
|
|
type GetSkillByWorkspaceAndNameParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// Used by skill import and runtime-local skill discovery to reuse a workspace
|
|
// skill by name rather than violating UNIQUE(workspace_id, name).
|
|
func (q *Queries) GetSkillByWorkspaceAndName(ctx context.Context, arg GetSkillByWorkspaceAndNameParams) (Skill, error) {
|
|
row := q.db.QueryRow(ctx, getSkillByWorkspaceAndName, arg.WorkspaceID, arg.Name)
|
|
var i Skill
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.Content,
|
|
&i.Config,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.PluginInstallationID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getSkillFile = `-- name: GetSkillFile :one
|
|
SELECT id, skill_id, path, content, created_at, updated_at FROM skill_file
|
|
WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) GetSkillFile(ctx context.Context, id pgtype.UUID) (SkillFile, error) {
|
|
row := q.db.QueryRow(ctx, getSkillFile, id)
|
|
var i SkillFile
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.SkillID,
|
|
&i.Path,
|
|
&i.Content,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getSkillInWorkspace = `-- name: GetSkillInWorkspace :one
|
|
SELECT id, workspace_id, name, description, content, config, created_by, created_at, updated_at, plugin_installation_id FROM skill
|
|
WHERE id = $1 AND workspace_id = $2
|
|
`
|
|
|
|
type GetSkillInWorkspaceParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) GetSkillInWorkspace(ctx context.Context, arg GetSkillInWorkspaceParams) (Skill, error) {
|
|
row := q.db.QueryRow(ctx, getSkillInWorkspace, arg.ID, arg.WorkspaceID)
|
|
var i Skill
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.Content,
|
|
&i.Config,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.PluginInstallationID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const listAgentSkillNamesByAgentIDs = `-- name: ListAgentSkillNamesByAgentIDs :many
|
|
SELECT ask.agent_id, s.name
|
|
FROM agent_skill ask
|
|
JOIN skill s ON s.id = ask.skill_id
|
|
WHERE ask.agent_id = ANY($1::uuid[])
|
|
AND ask.enabled = TRUE
|
|
ORDER BY ask.agent_id, s.name ASC
|
|
`
|
|
|
|
type ListAgentSkillNamesByAgentIDsRow struct {
|
|
AgentID pgtype.UUID `json:"agent_id"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
func (q *Queries) ListAgentSkillNamesByAgentIDs(ctx context.Context, agentIds []pgtype.UUID) ([]ListAgentSkillNamesByAgentIDsRow, error) {
|
|
rows, err := q.db.Query(ctx, listAgentSkillNamesByAgentIDs, agentIds)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListAgentSkillNamesByAgentIDsRow{}
|
|
for rows.Next() {
|
|
var i ListAgentSkillNamesByAgentIDsRow
|
|
if err := rows.Scan(&i.AgentID, &i.Name); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listAgentSkillSummaries = `-- name: ListAgentSkillSummaries :many
|
|
SELECT s.id, s.workspace_id, s.name, s.description, s.config, s.created_by, s.created_at, s.updated_at, ask.enabled
|
|
FROM skill s
|
|
JOIN agent_skill ask ON ask.skill_id = s.id
|
|
WHERE ask.agent_id = $1
|
|
ORDER BY s.name ASC
|
|
`
|
|
|
|
type ListAgentSkillSummariesRow struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Config []byte `json:"config"`
|
|
CreatedBy pgtype.UUID `json:"created_by"`
|
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
// Summary variant for the agent skills list endpoint — omits `content` for
|
|
// the same reason as ListSkillSummariesByWorkspace.
|
|
func (q *Queries) ListAgentSkillSummaries(ctx context.Context, agentID pgtype.UUID) ([]ListAgentSkillSummariesRow, error) {
|
|
rows, err := q.db.Query(ctx, listAgentSkillSummaries, agentID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListAgentSkillSummariesRow{}
|
|
for rows.Next() {
|
|
var i ListAgentSkillSummariesRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.Config,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Enabled,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listAgentSkills = `-- name: ListAgentSkills :many
|
|
|
|
SELECT s.id, s.workspace_id, s.name, s.description, s.content, s.config, s.created_by, s.created_at, s.updated_at, s.plugin_installation_id FROM skill s
|
|
JOIN agent_skill ask ON ask.skill_id = s.id
|
|
WHERE ask.agent_id = $1 AND ask.enabled = TRUE
|
|
ORDER BY s.name ASC
|
|
`
|
|
|
|
// Agent-Skill junction
|
|
func (q *Queries) ListAgentSkills(ctx context.Context, agentID pgtype.UUID) ([]Skill, error) {
|
|
rows, err := q.db.Query(ctx, listAgentSkills, agentID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []Skill{}
|
|
for rows.Next() {
|
|
var i Skill
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.Content,
|
|
&i.Config,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.PluginInstallationID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listAgentSkillsByIDs = `-- name: ListAgentSkillsByIDs :many
|
|
SELECT s.id, s.workspace_id, s.name, s.description, s.content, s.config, s.created_by, s.created_at, s.updated_at, s.plugin_installation_id FROM skill s
|
|
JOIN agent_skill ask ON ask.skill_id = s.id
|
|
WHERE ask.agent_id = $1
|
|
AND ask.enabled = TRUE
|
|
AND s.id = ANY($2::uuid[])
|
|
ORDER BY s.name ASC
|
|
`
|
|
|
|
type ListAgentSkillsByIDsParams struct {
|
|
AgentID pgtype.UUID `json:"agent_id"`
|
|
SkillIds []pgtype.UUID `json:"skill_ids"`
|
|
}
|
|
|
|
// Scoped variant of ListAgentSkills: the same junction predicate, narrowed to
|
|
// a set of requested skill IDs. The skill-bundle resolve path asks for one
|
|
// skill per request, so loading the agent's whole set there costs a full read
|
|
// and hash of every skill on every request. The junction predicate is also the
|
|
// authorization: an ID the agent does not have enabled simply returns no row,
|
|
// which the caller reports as not-found.
|
|
func (q *Queries) ListAgentSkillsByIDs(ctx context.Context, arg ListAgentSkillsByIDsParams) ([]Skill, error) {
|
|
rows, err := q.db.Query(ctx, listAgentSkillsByIDs, arg.AgentID, arg.SkillIds)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []Skill{}
|
|
for rows.Next() {
|
|
var i Skill
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.Content,
|
|
&i.Config,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.PluginInstallationID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listAgentSkillsByWorkspace = `-- name: ListAgentSkillsByWorkspace :many
|
|
SELECT ask.agent_id, s.id, s.name, s.description, ask.enabled
|
|
FROM agent_skill ask
|
|
JOIN skill s ON s.id = ask.skill_id
|
|
WHERE s.workspace_id = $1
|
|
ORDER BY s.name ASC
|
|
`
|
|
|
|
type ListAgentSkillsByWorkspaceRow struct {
|
|
AgentID pgtype.UUID `json:"agent_id"`
|
|
ID pgtype.UUID `json:"id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
func (q *Queries) ListAgentSkillsByWorkspace(ctx context.Context, workspaceID pgtype.UUID) ([]ListAgentSkillsByWorkspaceRow, error) {
|
|
rows, err := q.db.Query(ctx, listAgentSkillsByWorkspace, workspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListAgentSkillsByWorkspaceRow{}
|
|
for rows.Next() {
|
|
var i ListAgentSkillsByWorkspaceRow
|
|
if err := rows.Scan(
|
|
&i.AgentID,
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.Enabled,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listSkillFileMetadata = `-- name: ListSkillFileMetadata :many
|
|
SELECT id, skill_id, path,
|
|
octet_length(content)::bigint AS size,
|
|
encode(sha256(convert_to(content, 'UTF8')), 'hex') AS content_hash,
|
|
created_at, updated_at
|
|
FROM skill_file
|
|
WHERE skill_id = $1
|
|
ORDER BY path ASC
|
|
`
|
|
|
|
type ListSkillFileMetadataRow struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
SkillID pgtype.UUID `json:"skill_id"`
|
|
Path string `json:"path"`
|
|
Size int64 `json:"size"`
|
|
ContentHash string `json:"content_hash"`
|
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
}
|
|
|
|
// Metadata-only variant of ListSkillFiles: path, byte size and content hash
|
|
// without the body. Same reason as ListSkillSummariesByWorkspace — a skill
|
|
// whose supporting files total ~600KB cannot be listed at all when every row
|
|
// carries its full content, and the one command that would show which file is
|
|
// oversized was the command that timed out (GH multica-ai/multica#7498).
|
|
// size/hash are computed in Postgres so the file bodies never leave it.
|
|
//
|
|
// convert_to(content, 'UTF8'), never content::bytea: the cast runs the bytea
|
|
// INPUT parser over the text, so it reads backslash escapes instead of taking
|
|
// the bytes. A file containing `\x41` would hash as the single byte `A`, and
|
|
// one containing a bare backslash — a regex `\d+`, a Windows path, a LaTeX
|
|
// snippet — fails outright with "invalid input syntax for type bytea",
|
|
// turning an ordinary skill into a 500 on this endpoint.
|
|
func (q *Queries) ListSkillFileMetadata(ctx context.Context, skillID pgtype.UUID) ([]ListSkillFileMetadataRow, error) {
|
|
rows, err := q.db.Query(ctx, listSkillFileMetadata, skillID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListSkillFileMetadataRow{}
|
|
for rows.Next() {
|
|
var i ListSkillFileMetadataRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.SkillID,
|
|
&i.Path,
|
|
&i.Size,
|
|
&i.ContentHash,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listSkillFiles = `-- name: ListSkillFiles :many
|
|
|
|
SELECT id, skill_id, path, content, created_at, updated_at FROM skill_file
|
|
WHERE skill_id = $1
|
|
ORDER BY path ASC
|
|
`
|
|
|
|
// Skill File CRUD
|
|
func (q *Queries) ListSkillFiles(ctx context.Context, skillID pgtype.UUID) ([]SkillFile, error) {
|
|
rows, err := q.db.Query(ctx, listSkillFiles, skillID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []SkillFile{}
|
|
for rows.Next() {
|
|
var i SkillFile
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.SkillID,
|
|
&i.Path,
|
|
&i.Content,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listSkillFilesBySkillIDs = `-- name: ListSkillFilesBySkillIDs :many
|
|
SELECT id, skill_id, path, content, created_at, updated_at FROM skill_file
|
|
WHERE skill_id = ANY($1::uuid[])
|
|
ORDER BY skill_id, path ASC
|
|
`
|
|
|
|
// Batch variant of ListSkillFiles: loads every file for a set of skills in one
|
|
// round trip so LoadAgentSkills doesn't issue one query per skill on the
|
|
// task-claim hot path. Ordered by skill_id so the caller can group in a single
|
|
// linear pass. Like ListSkillFiles it returns full file bodies — callers that
|
|
// only need metadata must use ListSkillFileMetadata instead. Uses
|
|
// idx_skill_file_skill.
|
|
func (q *Queries) ListSkillFilesBySkillIDs(ctx context.Context, skillIds []pgtype.UUID) ([]SkillFile, error) {
|
|
rows, err := q.db.Query(ctx, listSkillFilesBySkillIDs, skillIds)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []SkillFile{}
|
|
for rows.Next() {
|
|
var i SkillFile
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.SkillID,
|
|
&i.Path,
|
|
&i.Content,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listSkillSummariesByWorkspace = `-- name: ListSkillSummariesByWorkspace :many
|
|
SELECT id, workspace_id, name, description, config, created_by, created_at, updated_at
|
|
FROM skill
|
|
WHERE workspace_id = $1
|
|
ORDER BY name ASC
|
|
`
|
|
|
|
type ListSkillSummariesByWorkspaceRow struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Config []byte `json:"config"`
|
|
CreatedBy pgtype.UUID `json:"created_by"`
|
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
}
|
|
|
|
// Same as ListSkillsByWorkspace but omits the SKILL.md `content` column. Used
|
|
// by list endpoints (CLI table, web list page) where the body is never read;
|
|
// shipping it everywhere blew up payload size on workspaces with many skills
|
|
// and caused 15s CLI timeouts from high-latency regions (GH multica-ai/multica#2174).
|
|
func (q *Queries) ListSkillSummariesByWorkspace(ctx context.Context, workspaceID pgtype.UUID) ([]ListSkillSummariesByWorkspaceRow, error) {
|
|
rows, err := q.db.Query(ctx, listSkillSummariesByWorkspace, workspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListSkillSummariesByWorkspaceRow{}
|
|
for rows.Next() {
|
|
var i ListSkillSummariesByWorkspaceRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.Config,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listSkillsByWorkspace = `-- name: ListSkillsByWorkspace :many
|
|
|
|
SELECT id, workspace_id, name, description, content, config, created_by, created_at, updated_at, plugin_installation_id FROM skill
|
|
WHERE workspace_id = $1
|
|
ORDER BY name ASC
|
|
`
|
|
|
|
// Skill CRUD
|
|
func (q *Queries) ListSkillsByWorkspace(ctx context.Context, workspaceID pgtype.UUID) ([]Skill, error) {
|
|
rows, err := q.db.Query(ctx, listSkillsByWorkspace, workspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []Skill{}
|
|
for rows.Next() {
|
|
var i Skill
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.Content,
|
|
&i.Config,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.PluginInstallationID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const removeAgentSkill = `-- name: RemoveAgentSkill :exec
|
|
DELETE FROM agent_skill
|
|
WHERE agent_id = $1 AND skill_id = $2
|
|
`
|
|
|
|
type RemoveAgentSkillParams struct {
|
|
AgentID pgtype.UUID `json:"agent_id"`
|
|
SkillID pgtype.UUID `json:"skill_id"`
|
|
}
|
|
|
|
func (q *Queries) RemoveAgentSkill(ctx context.Context, arg RemoveAgentSkillParams) error {
|
|
_, err := q.db.Exec(ctx, removeAgentSkill, arg.AgentID, arg.SkillID)
|
|
return err
|
|
}
|
|
|
|
const removeAllAgentSkills = `-- name: RemoveAllAgentSkills :exec
|
|
DELETE FROM agent_skill WHERE agent_id = $1
|
|
`
|
|
|
|
func (q *Queries) RemoveAllAgentSkills(ctx context.Context, agentID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, removeAllAgentSkills, agentID)
|
|
return err
|
|
}
|
|
|
|
const setAgentSkillEnabled = `-- name: SetAgentSkillEnabled :execrows
|
|
UPDATE agent_skill
|
|
SET enabled = $3
|
|
WHERE agent_id = $1 AND skill_id = $2
|
|
`
|
|
|
|
type SetAgentSkillEnabledParams struct {
|
|
AgentID pgtype.UUID `json:"agent_id"`
|
|
SkillID pgtype.UUID `json:"skill_id"`
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
func (q *Queries) SetAgentSkillEnabled(ctx context.Context, arg SetAgentSkillEnabledParams) (int64, error) {
|
|
result, err := q.db.Exec(ctx, setAgentSkillEnabled, arg.AgentID, arg.SkillID, arg.Enabled)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const updateSkill = `-- name: UpdateSkill :one
|
|
UPDATE skill SET
|
|
name = COALESCE($2, name),
|
|
description = COALESCE($3, description),
|
|
content = COALESCE($4, content),
|
|
config = COALESCE($5, config),
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING id, workspace_id, name, description, content, config, created_by, created_at, updated_at, plugin_installation_id
|
|
`
|
|
|
|
type UpdateSkillParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
Name pgtype.Text `json:"name"`
|
|
Description pgtype.Text `json:"description"`
|
|
Content pgtype.Text `json:"content"`
|
|
Config []byte `json:"config"`
|
|
}
|
|
|
|
func (q *Queries) UpdateSkill(ctx context.Context, arg UpdateSkillParams) (Skill, error) {
|
|
row := q.db.QueryRow(ctx, updateSkill,
|
|
arg.ID,
|
|
arg.Name,
|
|
arg.Description,
|
|
arg.Content,
|
|
arg.Config,
|
|
)
|
|
var i Skill
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Description,
|
|
&i.Content,
|
|
&i.Config,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.PluginInstallationID,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const upsertSkillFile = `-- name: UpsertSkillFile :one
|
|
INSERT INTO skill_file (skill_id, path, content)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT (skill_id, path) DO UPDATE SET
|
|
content = EXCLUDED.content,
|
|
updated_at = now()
|
|
RETURNING id, skill_id, path, content, created_at, updated_at
|
|
`
|
|
|
|
type UpsertSkillFileParams struct {
|
|
SkillID pgtype.UUID `json:"skill_id"`
|
|
Path string `json:"path"`
|
|
Content string `json:"content"`
|
|
}
|
|
|
|
func (q *Queries) UpsertSkillFile(ctx context.Context, arg UpsertSkillFileParams) (SkillFile, error) {
|
|
row := q.db.QueryRow(ctx, upsertSkillFile, arg.SkillID, arg.Path, arg.Content)
|
|
var i SkillFile
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.SkillID,
|
|
&i.Path,
|
|
&i.Content,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|