172 lines
4.7 KiB
Go
172 lines
4.7 KiB
Go
package sqlite
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"inbox/internal/domain/skill"
|
|
)
|
|
|
|
type builtinSkillSeed struct {
|
|
Definition skill.Definition
|
|
RoleNames []string
|
|
}
|
|
|
|
const inboxSkillMarkdown = `---
|
|
name: inbox
|
|
description: "Use Inbox V2 from a role runtime. Supports two operations only: send and ask. Use this when you need to post a workflow message or a blocking question into an existing topic with inbox api."
|
|
---
|
|
|
|
# Inbox
|
|
|
|
Use this skill when a role needs to communicate through Inbox V2.
|
|
|
|
This skill has two operations only:
|
|
- send: post a normal workflow message into an existing topic
|
|
- ask: post a blocking question that needs an answer from another role
|
|
|
|
## Before using either operation
|
|
|
|
Resolve the topic id first if you only know the workspace and topic slug:
|
|
|
|
inbox api GET "/api/v2/topics?workspace_id=<workspace_id>"
|
|
|
|
Pick the topic record you want, then use its id in the message API below.
|
|
|
|
## send
|
|
|
|
Use send for handoffs, updates, decisions, or summaries.
|
|
|
|
inbox api POST /api/v2/topics/<topic_id>/messages --data '{
|
|
"workspace_id": "<workspace_id>",
|
|
"from_role_name": "<current_role>",
|
|
"to_expr": "<target_role>",
|
|
"type": "chat",
|
|
"stage": "execution",
|
|
"body_markdown": "<message markdown>"
|
|
}'
|
|
|
|
Rules:
|
|
- Set type to the actual intent: chat, proposal, decision, or summary.
|
|
- Keep stage aligned with the current workflow state.
|
|
- Add reply_to_message_id when replying to a specific message.
|
|
|
|
## ask
|
|
|
|
Use ask when you are blocked and need a concrete answer.
|
|
|
|
inbox api POST /api/v2/topics/<topic_id>/messages --data '{
|
|
"workspace_id": "<workspace_id>",
|
|
"from_role_name": "<current_role>",
|
|
"to_expr": "<target_role>",
|
|
"type": "question",
|
|
"stage": "<current_stage>",
|
|
"body_markdown": "<single concrete question and the decision you need>"
|
|
}'
|
|
|
|
Rules:
|
|
- Ask one concrete blocker per message.
|
|
- State the decision, artifact, or approval you need back.
|
|
- Keep stage aligned with the task or planning stage you are in.
|
|
- Prefer ask over vague status pings.`
|
|
|
|
var builtinSkillSeeds = []builtinSkillSeed{
|
|
{
|
|
Definition: skill.Definition{
|
|
SkillKey: "inbox",
|
|
Name: "Inbox",
|
|
Description: "Use Inbox V2 from a role runtime with two operations: send and ask.",
|
|
SourceType: "capability",
|
|
ContentMarkdown: inboxSkillMarkdown,
|
|
Status: "active",
|
|
},
|
|
RoleNames: []string{
|
|
"leader",
|
|
"worker",
|
|
},
|
|
},
|
|
}
|
|
|
|
func (s *Store) ensureBuiltinSkills(ctx context.Context) error {
|
|
tx, err := s.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("begin builtin skill seed: %w", err)
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
now := s.now()
|
|
for _, seed := range builtinSkillSeeds {
|
|
if err := s.seedBuiltinSkillTx(ctx, tx, now, seed); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
return fmt.Errorf("commit builtin skill seed: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) seedBuiltinSkillTx(ctx context.Context, tx *sql.Tx, now string, seed builtinSkillSeed) error {
|
|
item, err := getSkillByKeyTx(ctx, tx, seed.Definition.SkillKey)
|
|
if err != nil {
|
|
if !errors.Is(err, sql.ErrNoRows) {
|
|
return err
|
|
}
|
|
item = seed.Definition
|
|
item.ID = "builtin-skill-" + seed.Definition.SkillKey
|
|
item.Version = 1
|
|
item.CreatedAt = now
|
|
item.UpdatedAt = now
|
|
if _, err := tx.ExecContext(ctx, `
|
|
INSERT INTO skills(id, skill_key, name, description, source_type, content_markdown, status, version, created_at, updated_at)
|
|
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`,
|
|
item.ID,
|
|
item.SkillKey,
|
|
item.Name,
|
|
item.Description,
|
|
item.SourceType,
|
|
item.ContentMarkdown,
|
|
item.Status,
|
|
item.Version,
|
|
item.CreatedAt,
|
|
item.UpdatedAt,
|
|
); err != nil {
|
|
return fmt.Errorf("insert builtin skill %q: %w", item.SkillKey, err)
|
|
}
|
|
}
|
|
|
|
for _, roleName := range seed.RoleNames {
|
|
if err := seedBuiltinRoleSkillBindingTx(ctx, tx, now, roleName, item.ID, item.SkillKey); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func seedBuiltinRoleSkillBindingTx(ctx context.Context, tx *sql.Tx, now, roleName, skillID, skillKey string) error {
|
|
if _, err := getRoleSkillBindingTx(ctx, tx, roleName, "", skillID); err == nil {
|
|
return nil
|
|
} else if !errors.Is(err, sql.ErrNoRows) {
|
|
return err
|
|
}
|
|
|
|
if _, err := tx.ExecContext(ctx, `
|
|
INSERT INTO role_skill_bindings(id, role_name, workspace_id, skill_id, is_enabled, sort_order, config_json, version, updated_by, created_at, updated_at)
|
|
VALUES(?, ?, NULL, ?, 1, 100, ?, 1, 'builtin-seed', ?, ?)
|
|
`,
|
|
"builtin-role-skill-binding-"+roleName+"-"+skillKey,
|
|
roleName,
|
|
skillID,
|
|
`{"category":"capability"}`,
|
|
now,
|
|
now,
|
|
); err != nil {
|
|
return fmt.Errorf("insert builtin role skill binding %q/%q: %w", roleName, skillKey, err)
|
|
}
|
|
return nil
|
|
}
|