Changeset 0.25.0 (#160)

This commit is contained in:
2026-03-08 16:54:17 +00:00
parent 937be26578
commit 2b01d540d6
63 changed files with 6942 additions and 2773 deletions

View File

@@ -287,12 +287,21 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
var personaID string // tracks active persona for KB scoping
var personaThinkingBudget *int // persona-level thinking budget for hook injection (v0.22.1)
// v0.25.0: Query channel metadata once — used by group leader check, tool context,
// and execution context. Avoids redundant SELECTs on channels.
var channelType string
var channelTeamID *string
_ = database.DB.QueryRowContext(c.Request.Context(), database.Q(`
SELECT COALESCE(type, 'direct'), team_id FROM channels WHERE id = $1
`), channelID).Scan(&channelType, &channelTeamID)
teamID := ""
if channelTeamID != nil {
teamID = *channelTeamID
}
// v0.23.2: Group leader default — when type=group and no @mention, use the leader persona
if req.PersonaID == "" && extractFirstMention(req.Content) == "" {
var channelType string
_ = database.DB.QueryRowContext(c.Request.Context(), database.Q(`
SELECT COALESCE(type, 'direct') FROM channels WHERE id = $1
`), channelID).Scan(&channelType)
if channelType == "group" {
// Find the leader persona via channel_participants → persona_group_members
var leaderPersonaID string
@@ -493,6 +502,16 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
// Resolve effective workspace: channel.workspace_id > project.workspace_id
workspaceID, _ := h.stores.Channels.ResolveWorkspaceID(c.Request.Context(), channelID)
// ── Tool context (v0.25.0) ─────────────────
// Uses channel metadata queried above (channelType, teamID).
tctx := tools.ToolContext{
ChannelType: channelType,
WorkspaceID: workspaceID,
TeamID: teamID,
IsVisitor: isSessionAuth(c),
// PersonaID set below after persona resolution
}
// ── @mention routing (v0.23.0 / v0.23.1) ─────────────────────────────────
// Resolve @persona-handle, @model-id, or @username.
// - User mention → skip completion, notify recipient (v0.23.1)
@@ -637,9 +656,10 @@ func (h *CompletionHandler) Complete(c *gin.Context) {
}
// Attach tool definitions if model supports tool calling and tools are available
tctx.PersonaID = personaID // finalize after all persona resolution (@mention, group leader, etc.)
hasBrowserTools := h.hub != nil && h.hub.IsConnected(userID)
if caps.ToolCalling && (tools.HasTools() || hasBrowserTools) {
provReq.Tools = h.buildToolDefs(c.Request.Context(), userID, hasBrowserTools, req.DisabledTools, workspaceID)
provReq.Tools = h.buildToolDefs(c.Request.Context(), userID, hasBrowserTools, req.DisabledTools, tctx, personaID)
}
// ── Permission pre-flight ─────────────────────────────────────────────
@@ -787,7 +807,12 @@ func (h *CompletionHandler) multiModelStream(
}
hasBrowserTools := h.hub != nil && h.hub.IsConnected(userID)
if caps.ToolCalling && (tools.HasTools() || hasBrowserTools) {
provReq.Tools = h.buildToolDefs(c.Request.Context(), userID, hasBrowserTools, req.DisabledTools, workspaceID)
tctx := tools.ToolContext{
WorkspaceID: workspaceID,
PersonaID: personaID,
IsVisitor: isSessionAuth(c),
}
provReq.Tools = h.buildToolDefs(c.Request.Context(), userID, hasBrowserTools, req.DisabledTools, tctx, personaID)
}
// Apply provider-specific request hooks (v0.22.1)
@@ -823,25 +848,21 @@ func (h *CompletionHandler) multiModelStream(
// buildToolDefs converts registered tools to provider-format tool definitions.
// If includeBrowser is true, also fetches tool schemas from enabled browser extensions.
// Tools whose names appear in disabledTools are excluded.
// Workspace tools are excluded when workspaceID is empty (no workspace bound).
func (h *CompletionHandler) buildToolDefs(ctx context.Context, userID string, includeBrowser bool, disabledTools []string, workspaceID string) []providers.ToolDef {
// v0.25.0: Tools self-declare availability via predicates on ToolContext.
// v0.25.0: Persona tool grants apply as a second-pass allowlist when personaID
// has explicit grants. Empty grants = inherit all (backward compat).
func (h *CompletionHandler) buildToolDefs(ctx context.Context, userID string, includeBrowser bool, disabledTools []string, tctx tools.ToolContext, personaID string) []providers.ToolDef {
// Build disabled set for O(1) lookup
disabled := make(map[string]bool, len(disabledTools))
for _, name := range disabledTools {
disabled[name] = true
}
// If no workspace is bound, disable workspace + git tools
if workspaceID == "" {
for _, name := range tools.WorkspaceToolNames() {
disabled[name] = true
}
for _, name := range tools.GitToolNames() {
disabled[name] = true
}
}
allTools := tools.AllDefinitionsFiltered(disabled)
// v0.25.0: Tools self-declare availability via predicates.
// AvailableFor evaluates each tool's Availability() against tctx,
// then applies the disabled set. Replaces manual WorkspaceToolNames()
// and GitToolNames() suppression.
allTools := tools.AvailableFor(tctx, disabled)
defs := make([]providers.ToolDef, 0, len(allTools))
for _, t := range allTools {
defs = append(defs, providers.ToolDef{
@@ -892,6 +913,28 @@ func (h *CompletionHandler) buildToolDefs(ctx context.Context, userID string, in
}
}
// v0.25.0: Persona tool grants — second-pass allowlist.
// If the active persona has explicit tool grants, restrict to only those tools.
// Empty grants = persona inherits all context-available tools (backward compat).
if personaID != "" && h.stores.Personas != nil {
grants, err := h.stores.Personas.GetToolGrants(ctx, personaID)
if err != nil {
log.Printf("⚠️ Failed to load tool grants for persona %s: %v", personaID, err)
} else if len(grants) > 0 {
allowed := make(map[string]bool, len(grants))
for _, g := range grants {
allowed[g] = true
}
filtered := defs[:0]
for _, d := range defs {
if allowed[d.Function.Name] {
filtered = append(filtered, d)
}
}
defs = filtered
}
}
return defs
}