Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis pull request introduces skill management and refactors user identity handling throughout the agent subsystem. Changes include adding SkillStore interface with search/pagination, implementing skill-specific tools (use_skill, search_skills), consolidating user context via UserIdentity struct, implementing delegate snapshot tracking for sub-agents, introducing SubSessionRegistry for sub-session lifecycle management, and updating API schemas and frontend components to expose these features. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant ChatInput
participant Agent API
participant Delegate Tool
participant Sub-Agent
participant Session Manager
User->>ChatInput: Select skill + send message
ChatInput->>Agent API: SendMessage(userIdentity, prefixed with skill)
activate Agent API
Agent API->>Session Manager: Find/create session
Agent API->>Session Manager: Create loop with SkillStore
deactivate Agent API
activate Session Manager
Session Manager->>Sub-Agent: Initialize with skill summaries
Sub-Agent->>Sub-Agent: Pre-load selected skill knowledge
deactivate Session Manager
User->>ChatInput: Trigger delegation
ChatInput->>Agent API: SendMessage with delegate task
activate Agent API
Agent API->>Delegate Tool: Execute delegate task
activate Delegate Tool
Delegate Tool->>Session Manager: RegisterSubSession
Delegate Tool->>Sub-Agent: Create + run sub-agent
Sub-Agent->>Sub-Agent: Execute with pre-loaded skills
Sub-Agent-->>Delegate Tool: Return result + cost
Delegate Tool->>Session Manager: SetDelegateCompleted(id, cost)
Delegate Tool-->>Agent API: Return delegate snapshot
deactivate Delegate Tool
deactivate Agent API
Agent API->>ChatInput: Stream delegate snapshot
ChatInput->>ChatInput: Update delegate panel
ChatInput->>User: Display running/completed sub-agent
sequenceDiagram
actor User
participant ChatInput
participant SkillPicker
participant API Handler
participant SkillStore
User->>ChatInput: Type / to search skills
ChatInput->>SkillPicker: Open with filter query
activate SkillPicker
SkillPicker->>API Handler: GET /settings/agent/skills?q=query
activate API Handler
API Handler->>SkillStore: Search(query, tags, pagination)
activate SkillStore
SkillStore->>SkillStore: Query in-memory index
SkillStore-->>API Handler: Return paginated results
deactivate SkillStore
API Handler-->>SkillPicker: SkillMetadata[]
deactivate API Handler
SkillPicker->>SkillPicker: Render dropdown with matches
deactivate SkillPicker
User->>SkillPicker: Select skill
SkillPicker->>ChatInput: onSelect(skill)
ChatInput->>ChatInput: Add skill chip, focus textarea
User->>ChatInput: Send message
ChatInput->>ChatInput: Prefix message with skill knowledge blocks
ChatInput->>API Handler: SendMessage(prefixed content)
API Handler->>Agent API: Pass to agent loop with skill context
Estimated code review effort🎯 4 (Complex) | ⏱️ ~55 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 17
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
internal/agent/provider_cache.go (1)
38-47:⚠️ Potential issue | 🟡 Minor
Setevicts unnecessarily when updating an existing key at full capacity.
evictIfFullLockedis called unconditionally before the map store. When the hash already exists in the cache (i.e., an in-place update), the size will not grow, yet one random live entry is still evicted. With the old "clear-all" policy this was moot, but with single-entry eviction the effect is visible: a valid, unrelated entry is silently dropped on every update-in-place when the cache is at capacity.
GetOrCreatealready handles this correctly via its double-check guard (Line 64).🐛 Proposed fix
func (c *ProviderCache) Set(cfg LLMConfig, provider llm.Provider) { hash := HashLLMConfig(cfg) c.mu.Lock() defer c.mu.Unlock() - c.evictIfFullLocked() + if _, exists := c.cache[hash]; !exists { + c.evictIfFullLocked() + } c.cache[hash] = cachedProvider{ provider: provider, model: cfg.Model, } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/agent/provider_cache.go` around lines 38 - 47, ProviderCache.Set currently calls evictIfFullLocked unconditionally which can evict a live entry when updating an existing key; change Set (compute hash via HashLLMConfig(cfg) while holding c.mu) to first check if the hash already exists in c.cache and only call evictIfFullLocked when the key is absent (i.e., about to grow the map), then assign c.cache[hash] = cachedProvider{provider: provider, model: cfg.Model}; mirror the double-check logic used by GetOrCreate to avoid unnecessary eviction on in-place updates.ui/src/pages/agent-skills/index.tsx (2)
199-203:⚠️ Potential issue | 🟡 MinorEnabled count badge reflects only the current page, not total enabled skills.
skills.filter((s) => s.enabled).lengthat Line 201 counts only the skills returned in the current page. With pagination, this can be misleading (e.g., showing "enabled (2)" when there are 20 enabled skills across all pages). Consider using a server-provided total or removing the badge when paginated.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/pages/agent-skills/index.tsx` around lines 199 - 203, The enabled-count badge currently computes ({skills.filter((s) => s.enabled).length}) from the page-scoped skills array, which shows only the current page's enabled items; change the JSX to use a server-provided total enabled count (e.g., a response field like totalEnabled or enabledCount from your skills API) instead of filtering the paged skills, or hide the badge when pagination is active — update the render block around tab === 'enabled' to reference that totalEnabled prop/state (or conditional on single-page mode) rather than skills.filter(...).
99-116:⚠️ Potential issue | 🔴 CriticalToggling a skill's enabled state sends only the current page's enabled IDs, clobbering skills on other pages.
newEnabled(line 103) is computed fromskills, which contains only the current page of results. ThePUT /settings/agent/enabled-skillsendpoint performs a full replacement (cfg.EnabledSkills = request.Body.SkillIds), so every enabled skill on other pages will be silently disabled when toggling a skill on the current page. WithDEFAULT_PER_PAGE = 30, toggling any skill affects the entire system state.Fetch the current full enabled set before computing the delta, or change the API to support a patch operation for a single skill instead of full replacement.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/pages/agent-skills/index.tsx` around lines 99 - 116, The bug is that newEnabled is computed only from the paginated skills list and the PUT to '/settings/agent/enabled-skills' replaces the entire enabled list, clobbering skills on other pages; fix by first fetching the current full enabled set from the server (e.g. GET '/settings/agent/enabled-skills' or another endpoint) and merging the optimistic change for skill.id/ willEnable into that full set before calling client.PUT, then send the merged skillIds array instead of computing newEnabled from the paginated skills; retain the existing error handling (mutate, setError) if the PUT fails.internal/service/frontend/api/v1/agent_skills.go (1)
113-118:⚠️ Potential issue | 🟡 MinorPropagate list errors from
collectSkillIDsinstead of silently returning empty maps.The
agentSkillStore.List(ctx)method can return errors (I/O, configuration, or backend failures), butcollectSkillIDsswallows them and returns an empty map. This causes incorrect behavior in two places:
- At line 116 (ID generation): May generate IDs using an incomplete set, risking duplicates
- At line 281 (ID validation): Returns "skill not found" when the actual error is a backend failure
Change
collectSkillIDsto return(map[string]struct{}, error)and propagate errors to the callers as 500 responses.🛠️ Suggested fix
-func (a *API) collectSkillIDs(ctx context.Context) map[string]struct{} { +func (a *API) collectSkillIDs(ctx context.Context) (map[string]struct{}, error) { skills, err := a.agentSkillStore.List(ctx) if err != nil { - return make(map[string]struct{}) + return nil, err } ids := make(map[string]struct{}, len(skills)) for _, s := range skills { ids[s.ID] = struct{}{} } - return ids + return ids, nil }Then update callers at lines 116 and 281 to check the error and return HTTP 500 on failure.
Also applies to: 116, 281
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/service/frontend/api/v1/agent_skills.go` around lines 113 - 118, collectSkillIDs currently swallows errors from agentSkillStore.List; change its signature to collectSkillIDs(ctx) (map[string]struct{}, error), have it return the underlying error when List fails, and update its callers to handle that error: in the create path (where id := valueOf(body.Id) and id = agent.UniqueID(body.Name, existingIDs, "skill")) call collectSkillIDs and if it returns an error respond with HTTP 500; likewise update the ID validation path (the code that currently treats missing IDs as "skill not found") to check the error from collectSkillIDs and return HTTP 500 on error instead of treating it as a missing-skill case. Ensure references to agent.UniqueID and agentSkillStore.List remain untouched except for propagating the error.ui/src/features/agent/components/ChatMessages.tsx (1)
35-37:⚠️ Potential issue | 🟡 MinorAuto-scroll does not trigger when
pendingUserMessageappears orisWorkingchanges.
useEffectonly runs whenmessageschanges. When the user submits a message,pendingUserMessagebecomes non-null beforemessagesupdates, so the optimistic bubble renders at the bottom without scrolling into view. The same applies to theisWorkingspinner.🔧 Suggested fix
useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [messages]); + }, [messages, pendingUserMessage, isWorking]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/features/agent/components/ChatMessages.tsx` around lines 35 - 37, The auto-scroll effect currently only depends on messages; update the effect (or add a new one) that calls messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) to also run when pendingUserMessage or isWorking change so the optimistic user bubble and spinner are scrolled into view; locate the useEffect in ChatMessages.tsx and add pendingUserMessage and isWorking to the dependency array (or create a second effect that depends on those two) and keep the same scrollIntoView behavior and null checks on messagesEndRef.internal/agent/system_prompt.txt (1)
103-112:⚠️ Potential issue | 🟠 Major
use_skillis referenced in the prompt body but missing from the<tools>section.
use_skillis the primary mechanism mentioned in<available_skills>(line 116) and<skill_delegation>(lines 124–125), yet it is absent from the<tools>enumeration. The<tools>block is the agent's canonical reference for available capabilities; omittinguse_skillthere while directing the agent to use it creates an inconsistency that may reduce its usage.📝 Proposed fix
- `search_skills`: Discover available skills by keyword or tag. Returns summaries without loading full knowledge. +- `use_skill`: Load the full knowledge content of a skill by ID into the current context. </tools>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/agent/system_prompt.txt` around lines 103 - 112, The tools list is missing the `use_skill` entry referenced elsewhere; add a `use_skill` tool description to the <tools> block so it matches mentions in <available_skills> and <skill_delegation>. Specifically, insert a short line describing `use_skill`: its purpose (invoke a named skill), expected parameters (skill name and optional args), and any timeout/behavior expectations so callers using `use_skill` in the prompt body, `<available_skills>`, and `<skill_delegation>` have a canonical reference (look for the <tools> block and the symbols `use_skill`, `<available_skills>`, and `<skill_delegation>` to locate where to add this).
🧹 Nitpick comments (24)
internal/persis/fileagentmodel/store_test.go (1)
329-346: LGTM — correct error assertions and good new coverage.
ErrModelNameAlreadyExistsis the right sentinel now. The "empty name returns error" sub-test is properly parallelised and covers the new validation path. The string-match assertion (assert.Contains) is acceptable since no sentinel error exists for the "name required" case; optionally, definingErrModelNameRequiredwould let you useassert.ErrorIsconsistently with the rest of the tests.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/persis/fileagentmodel/store_test.go` around lines 329 - 346, Add a sentinel error ErrModelNameRequired and return it from the model-name validation path used by store.Update so callers can check the exact error; then update the "empty name returns error" sub-test in store_test.go to assert.ErrorIs(t, err, agent.ErrModelNameRequired) instead of assert.Contains. Locate the validation logic invoked by the Update method (the code that currently produces the "model name is required" string) and replace the string-only error with the new ErrModelNameRequired value so the test and other callers can use error identity checks.ui/src/features/agent/components/messages/SubAgentChips.tsx (1)
30-30: Consider narrowing theuseMemodependency totoolCall.function.arguments.The current dependency
[toolCall]triggers recomputation on any object reference change, even if the arguments string hasn't changed. SinceparseDelegateTasksonly readstoolCall.function.arguments, using that as the dependency is more precise.♻️ Suggested change
- const tasks = useMemo(() => parseDelegateTasks(toolCall), [toolCall]); + const tasks = useMemo(() => parseDelegateTasks(toolCall), [toolCall.function.arguments]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/features/agent/components/messages/SubAgentChips.tsx` at line 30, The useMemo call in SubAgentChips (const tasks = useMemo(() => parseDelegateTasks(toolCall), [toolCall])) is too broad and causes unnecessary recomputation; change the dependency array to depend specifically on toolCall.function.arguments (e.g., [toolCall.function.arguments]) so parseDelegateTasks only reruns when the arguments string actually changes, keeping the same parseDelegateTasks and useMemo usage.internal/core/spec/step.go (1)
1636-1640: Duplicated regex acknowledged — consider a shared internal package if this pattern spreads further.The comment on Line 1637 explains the duplication is to avoid an import cycle (
spec → agent → spec). This is pragmatic for now, but if more validation constants end up duplicated, consider extracting them into a small leaf package (e.g.,internal/core/slugid) that bothspecandagentcan import.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/core/spec/step.go` around lines 1636 - 1640, The comment notes a duplicated regexp; to prevent future duplication extract the shared regexp and constants into a small leaf package (e.g., internal/core/slugid) and update both packages to import it: move validSkillIDRegexp and maxSkillIDLength (and the original agent.validSlugRegexp) into the new package, expose them with clear names (e.g., SlugRegexp, MaxSlugLength), update references in spec (validSkillIDRegexp) and agent to use the new package, and run/update tests to ensure no import cycles or behavior changes.internal/agent/skill.go (1)
96-118:LoadSkillSummariesissues N individualGetByIDcalls — acceptable for small lists, but document the expectation.For typical per-agent skill lists (single digits), this is fine. If this ever needs to handle larger sets, a batch-fetch method on
SkillStorewould avoid the N+1 pattern.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/agent/skill.go` around lines 96 - 118, LoadSkillSummaries currently issues N individual SkillStore.GetByID calls (N+1 pattern) which is fine for small per-agent lists but should be documented and made easy to change; update the comment and/or function documentation for LoadSkillSummaries to state it performs one GetByID per enabled ID and is intended for small lists (single-digit), and add a TODO suggesting adding a batch method on SkillStore (e.g., GetByIDs or ListByIDs) to replace the loop if larger sets are needed; reference LoadSkillSummaries and SkillStore.GetByID in the note so future maintainers know where to modify.ui/src/features/agent/components/messages/AssistantMessage.tsx (2)
25-26: Hardcoded'delegate'tool name — extract to a constant.If the tool name is ever renamed, both filter lines silently stop working. A shared constant (e.g., in
constants.tsor anagentTools.ts) would make the coupling explicit.♻️ Proposed change
In
constants.ts:+export const DELEGATE_TOOL_NAME = 'delegate';In
AssistantMessage.tsx:+import { DELEGATE_TOOL_NAME } from '../../constants'; ... -const delegateCalls = toolCalls?.filter((tc) => tc.function.name === 'delegate') ?? []; -const otherCalls = toolCalls?.filter((tc) => tc.function.name !== 'delegate') ?? []; +const delegateCalls = toolCalls?.filter((tc) => tc.function.name === DELEGATE_TOOL_NAME) ?? []; +const otherCalls = toolCalls?.filter((tc) => tc.function.name !== DELEGATE_TOOL_NAME) ?? [];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/features/agent/components/messages/AssistantMessage.tsx` around lines 25 - 26, The two filters in AssistantMessage.tsx use the hardcoded tool name 'delegate' (seen in delegateCalls and otherCalls) — extract that string into a shared constant (e.g., DELEGATE_TOOL_NAME) placed in a constants file (constants.ts or agentTools.ts) and import it into AssistantMessage.tsx; update both filters to compare tc.function.name against that constant so renaming the tool only requires changing the constant.
15-17:content: stringshould be typed ascontent?: string.
Message.contentisstring | undefined(optional in the interface). Typing this prop as required forces callers to writecontent={message.content ?? ''}. Since the render already guards with{content && ...}, marking it optional is cleaner and avoids the mismatch.♻️ Proposed change
- content: string; + content?: string;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/features/agent/components/messages/AssistantMessage.tsx` around lines 15 - 17, The AssistantMessage component currently types the destructured prop as content: string which conflicts with Message.content being string | undefined; update the prop typing in the AssistantMessage props signature to content?: string (make it optional) so callers don't need to pass a fallback and the component's existing guard {content && ...} remains correct; locate the prop definition in the AssistantMessage function/component and change the content type to optional to match Message.content.internal/core/spec/step_test.go (1)
3563-3644: Add boundary test cases for the max-length constraint.
slug.goenforcesmaxSlugLength = 128. The current suite covers regex/format paths but has no case for an ID at exactly 128 characters (valid) or 129+ characters (should error). This is the primary gap against the length validation branch.➕ Suggested boundary cases to add
+ { + name: "exactly max length is valid", + skills: []string{strings.Repeat("a", 128)}, + wantErr: false, + }, + { + name: "exceeds max length is invalid", + skills: []string{strings.Repeat("a", 129)}, + wantErr: true, + errMsg: "invalid skill ID", + },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/core/spec/step_test.go` around lines 3563 - 3644, Add two boundary test cases to TestValidateAgent_SkillIDs that exercise the maxSlugLength from slug.go: one skill ID exactly 128 characters (should be valid) and one 129+ characters (should error with "invalid skill ID"); create the long strings in the test (e.g., repeat a safe character like 'a' to length 128 and 129), set them as step.Agent.Skills, call validateAgent(step), and assert no error for the 128-char case and an error containing "invalid skill ID" for the 129+ case.ui/src/features/agent/components/messages/ToolResultMessage.tsx (1)
7-10:truncateContentduplicates the pattern oftruncateTaskin constants.ts.Both functions perform the same substring + ellipsis truncation. Consider generalizing: extract a shared
truncate(text: string, maxLength: number): stringutility inconstants.ts, and havetruncateTaskcall it withTASK_TRUNCATE_LENGTH. This avoids drift between the two implementations (slicevssubstring,>vs<=).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/features/agent/components/messages/ToolResultMessage.tsx` around lines 7 - 10, Create a single shared truncate utility in constants.ts (export function truncate(text: string, maxLength: number): string) and have both truncateContent in ToolResultMessage.tsx and truncateTask in constants.ts call that shared truncate with the appropriate max length (e.g., TASK_TRUNCATE_LENGTH) instead of duplicating substring logic; update imports/exports accordingly and remove the duplicate implementation so both use the exact same behavior.ui/src/features/agent/hooks/useDelegateManager.ts (1)
47-60:handleDelegateSnapshotsincrements zIndex for every snapshot, even for existing delegates.Each call to
handleDelegateSnapshotsbumpszIndexCounterReffor every delegate in the snapshot array (Line 52), regardless of whether the delegate already exists. If snapshots arrive periodically via SSE, this causes unbounded z-index inflation. Consider preserving the existingzIndexfor known delegates:♻️ Preserve existing zIndex
for (const snap of snapshots) { + const existing = prev[snap.id]; next[snap.id] = { - info: { id: snap.id, task: snap.task, status: snap.status, zIndex: ++zIndexCounterRef.current, positionIndex: 0 }, - messages: prev[snap.id]?.messages || [], - isOpen: prev[snap.id]?.isOpen || false, + info: { + id: snap.id, + task: snap.task, + status: snap.status, + zIndex: existing?.info.zIndex ?? ++zIndexCounterRef.current, + positionIndex: 0, + }, + messages: existing?.messages || [], + isOpen: existing?.isOpen || false, };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/features/agent/hooks/useDelegateManager.ts` around lines 47 - 60, handleDelegateSnapshots currently increments zIndexCounterRef for every snapshot, causing z-index inflation for existing delegates; modify the update inside setDelegateStore so that for each snap you reuse prev[snap.id]?.info.zIndex when present and only assign ++zIndexCounterRef.current for new delegates (or when info.zIndex is undefined), update delegateStoreRef.current and return next as before; reference the handleDelegateSnapshots function, zIndexCounterRef, delegateStoreRef, and setDelegateStore to locate and change the logic that sets info.zIndex.ui/src/features/agent/components/AgentChatModal.tsx (1)
88-95: AddsetErrorto the dependency array for consistency with other callbacks.
setErroris created fromuseState(line 148 in useAgentChat.ts), making it stable across renders, so the missing dependency is technically benign. However,setErroris included in the dependency arrays ofhandleCancel(line 109) andhandleSelectSession(line 121) elsewhere in the same file. Add it to the effect's dependency array at line 95 for consistency.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/features/agent/components/AgentChatModal.tsx` around lines 88 - 95, The effect that calls findLatestSession and selectSession is missing setError in its dependency array; update the useEffect whose dependencies are [isOpen, sessions, sessionId, selectSession] to also include setError so it matches other callbacks (see setError, findLatestSession, selectSession) and keeps dependency lists consistent across handleCancel and handleSelectSession.internal/persis/fileagentskill/store_test.go (1)
645-738: Consider adding a zero-resultsSearchtest.The four sub-cases cover positive paths well, but there's no test asserting that
Searchreturns an empty result set for a non-matching query or tag. As per coding guidelines, tests should cover failure paths. Adding one case (e.g.,Query: "nomatch") would close this gap.🧪 Suggested additional sub-case
+ t.Run("no results when query matches nothing", func(t *testing.T) { + t.Parallel() + store, _ := setupTestStore(t) + createSkill(t, store, newTestSkill("k8s", "Kubernetes")) + + result, err := store.Search(ctx, agent.SearchSkillsOptions{ + Paginator: exec.DefaultPaginator(), + Query: "nomatch", + }) + require.NoError(t, err) + assert.Empty(t, result.Items) + assert.Equal(t, 0, result.TotalCount) + })Based on learnings: "Co-locate Go tests as
*_test.gofiles; favour table-driven cases and cover failure paths."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/persis/fileagentskill/store_test.go` around lines 645 - 738, Add a zero-results subtest to TestStore_Search that verifies Search returns an empty result set for non-matching queries or tags: inside TestStore_Search add a t.Run("zero-results", ...) which calls setupTestStore, creates one or two test skills via createSkill/newTestSkill, then calls store.Search(ctx, agent.SearchSkillsOptions{Paginator: exec.DefaultPaginator(), Query: "nomatch"}) and assert require.NoError(err) and require.Len(result.Items, 0) and assert.Equal(t, 0, result.TotalCount); you can also add a second check using Tags: []string{"no-such-tag"} to confirm tag-based zero results.internal/persis/filesession/store.go (1)
530-530:resultshould be initialised to an empty slice, notnil.
ListSessions(line 303) usesmake([]*agent.Session, 0, len(sessIDs)).ListSubSessionsleavesresultas anilslice when there are no matches, which is an inconsistency callers may need to guard against. A pre-allocated slice also avoids repeated reallocations when children are appended.- var result []*agent.Session + result := make([]*agent.Session, 0)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/persis/filesession/store.go` at line 530, ListSubSessions currently declares "var result []*agent.Session" leaving it nil when empty; change it to initialize an empty preallocated slice (e.g. result := make([]*agent.Session, 0, len(children))) so callers get a non-nil slice and appends avoid reallocations. Mirror the pattern used in ListSessions and use the "children" length for capacity when available; keep the variable name "result" and the function name "ListSubSessions".internal/persis/fileagentskill/examples.go (1)
17-20: Hardcoded IDs could drift from embedded examples.
ExampleSkillIDs()is a manually maintained list. If a new skill is added underexamples/, this list must be updated in lockstep. The tests catch this, but you could derive IDs from the embedded FS at init time to eliminate the drift risk.💡 Derive IDs from embedded FS
-func ExampleSkillIDs() []string { - return []string{"dagu-ai-workflows", "dagu-containers", "dagu-server-worker"} -} +var exampleSkillIDs []string + +func init() { + entries, err := fs.ReadDir(exampleSkillsFS, "examples") + if err != nil { + return + } + for _, e := range entries { + if e.IsDir() { + exampleSkillIDs = append(exampleSkillIDs, e.Name()) + } + } +} + +func ExampleSkillIDs() []string { + return append([]string(nil), exampleSkillIDs...) +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/persis/fileagentskill/examples.go` around lines 17 - 20, Replace the hardcoded list in ExampleSkillIDs() with a computed slice populated at package init by scanning the embedded examples filesystem (the embedded FS variable and the examples/ directory), deriving each skill ID from the directory or manifest found (e.g., directory names or a skill metadata file) and storing them in a package-level variable; update ExampleSkillIDs() to return that variable so new example skills added to the embedded FS are automatically included and tests no longer drift.internal/agent/system_prompt.go (1)
52-61: Consider an options struct as the parameter list grows.
GenerateSystemPromptnow takes 6 parameters. If more context is added in the future, consider grouping them into aSystemPromptInputstruct for readability.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/agent/system_prompt.go` around lines 52 - 61, GenerateSystemPrompt takes many parameters; introduce a new SystemPromptInput struct that groups EnvironmentInfo, CurrentDAG, MemoryContent, auth.Role (or precomputed User via buildUserCapabilities), AvailableSkills and SkillCount, then change the GenerateSystemPrompt signature to accept a single SystemPromptInput (or a pointer) and adapt the function body to populate systemPromptData from that struct; update all call sites to construct SystemPromptInput before calling GenerateSystemPrompt and ensure references to systemPromptData, buildUserCapabilities (if you prefer to pass Role and compute User inside the function) and AvailableSkills/SkillCount are adjusted accordingly.internal/agent/search_skills_tool_test.go (1)
16-143: MovetestSkillStoretointernal/testand consolidate the 19TestSearchSkills_*functions into a table-driven test.Two guideline violations confirmed in this file:
Duplicated mock across multiple files:
testSkillStore(withtestHasAllTagsandtestMatchesSkillhelpers) is currently duplicated across three test files:search_skills_tool_test.go,tool_registry_test.go, anddelegate_test.go. Per guidelines, this reusable fixture should live ininternal/testto eliminate duplication.Non-table-driven tests: The 19 standalone
TestSearchSkills_*functions follow an identical structure. They map cleanly to a table-driven test with fields likename,query,tags,allowedSkills,wantContains, andwantNotContains.Both align with the coding guidelines: "Use shared fixtures from
internal/testinstead of duplicating mocks" and "favour table-driven cases and cover failure paths".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/agent/search_skills_tool_test.go` around lines 16 - 143, Move the reusable test fixture (testSkillStore plus helper functions testHasAllTags and testMatchesSkill) into a shared package under internal/test and update any imports in files that used the duplicate copies (search_skills_tool_test.go, tool_registry_test.go, delegate_test.go) to reference the new internal/test package; then replace the 19 standalone TestSearchSkills_* functions in search_skills_tool_test.go with a single table-driven test that iterates cases (fields: name, query, tags, allowedSkills, wantContains, wantNotContains) and asserts results using the shared testSkillStore and its Search method (referencing Search on testSkillStore and the existing SearchSkillsOptions/Paginator usage). Ensure helper removal from the original file and fix any test references to testHasAllTags/testMatchesSkill to use the centralized helpers or exported equivalents.api/v1/api.gen.go (1)
438-447: Generated code looks consistent; considerIDnaming in the OpenAPI spec.This file is auto-generated (
api.gen.go), so issues should be addressed at the source (likelyapi/v1/api.yamlor generator config). One minor note:Idon line 441 should conventionally beIDin Go (pergolint/idiomatic Go). If your generator supports a naming override or if the spec field can be aliased, consider renaming it so the generated Go symbol becomesIDwhile the JSON tag remains"id".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@api/v1/api.gen.go` around lines 438 - 447, The generated struct AgentDelegateSnapshot uses a non-idiomatic field name `Id`; update the source OpenAPI spec or generator config so the property name maps to `ID` while keeping the JSON tag `"id"` (e.g., change the property name/alias for the `id` field in the OpenAPI definition that produces AgentDelegateSnapshot) or enable a naming override in the generator so the generated struct field becomes `ID` with `json:"id"`, leaving the rest of the generated code untouched.internal/agent/delegate_test.go (1)
605-612: Repeated event-filtering pattern could be extracted into a helper.The pattern for extracting
DelegateEvents fromregistry.eventsis duplicated across 4+ tests (Lines 607–612, 644–649, 683–688, etc.). A small helper would reduce boilerplate:func delegateEvents(registry *mockSubSessionRegistry) []DelegateEvent { var events []DelegateEvent for _, ev := range registry.events { if ev.DelegateEvent != nil { events = append(events, *ev.DelegateEvent) } } return events }Also applies to: 644-649, 683-688
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/agent/delegate_test.go` around lines 605 - 612, The test file repeats the same loop to extract DelegateEvent values from registry.events; add a helper function (e.g., delegateEvents(registry *mockSubSessionRegistry) []DelegateEvent) that iterates registry.events, appends non-nil ev.DelegateEvent to a slice and returns it, then replace the duplicated loops in tests (the blocks that reference registry.mu.Lock()/Unlock() and build events from ev.DelegateEvent) with calls to this helper (ensuring any required locking is done either inside the helper or kept around the call to preserve concurrency safety).internal/cmd/context.go (1)
425-464: Early returns prevent creating independent stores.If
fileagentmodel.Newfails (Line 440), the method returns before creating MemoryStore and SkillStore, even though these stores don't depend on the model store. This means a model-store initialization failure unnecessarily disables memory and skill features for CLI commands.Consider continuing past model store failure:
♻️ Suggested change
ams, err := fileagentmodel.New(filepath.Join(c.Config.Paths.DataDir, "agent", "models")) if err != nil { logger.Warn(c, "Failed to create agent model store", tag.Error(err)) - return result + } else { + result.ModelStore = ams } - result.ModelStore = ams ms, err := filememory.New(c.Config.Paths.DAGsDir) if err != nil { logger.Warn(c, "Failed to create agent memory store", tag.Error(err)) - return result - } - result.MemoryStore = ms + } else { + result.MemoryStore = ms + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/cmd/context.go` around lines 425 - 464, The agentStores function currently returns immediately when fileagentmodel.New fails, preventing creation of independent stores; change the fileagentmodel.New error branch so it logs the failure (keep logger.Warn with tag.Error(err)) but does not return—leave result.ModelStore nil and continue to initialize filememory.New and fileagentskill.New (so MemoryStore and SkillStore get created even if ModelStore initialization failed); keep the early return behavior for failures that truly prevent further initialization (e.g., fileagentconfig.New) and ensure result fields are set only when their constructors succeed.ui/src/features/agent/hooks/useSSEConnection.ts (1)
96-106: No feedback when SSE retries are exhausted.When
retryCountRef.current >= MAX_SSE_RETRIESand the EventSource isCLOSED, the handler silently gives up. Consider adding anonConnectionLostcallback toSSECallbacksso the consumer can show a "connection lost" indicator, or at minimum log the failure.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/features/agent/hooks/useSSEConnection.ts` around lines 96 - 106, The SSE onerror handler currently stops retrying silently when retryCountRef.current >= MAX_SSE_RETRIES; add a way to notify consumers by extending SSECallbacks with an onConnectionLost callback and invoke it when eventSource.readyState === EventSource.CLOSED and retryCountRef.current >= MAX_SSE_RETRIES (or at least log an error). Specifically, update the SSECallbacks interface to include onConnectionLost?: (sessionId?: string) => void, wire the callback into the hook that creates eventSource (alongside existing setSseRetryTrigger and eventSourceRef checks), and call props.sseCallbacks.onConnectionLost(sessionId) (or processLogger.error) in the else branch where retries are exhausted so the UI can show a "connection lost" indicator.internal/service/frontend/api/v1/agent_skills_test.go (1)
105-135:mockSkillStore.SearchignoresQuery,Tags, andAllowedIDsfilters.The mock only handles pagination but ignores all filtering fields from
SearchSkillsOptions. IfListAgentSkills(or future tests) passes query/tag/allowed-ID filters, the mock will return unfiltered results, potentially masking bugs.Consider adding at minimum
AllowedIDsfiltering since the production handler likely uses it:♻️ Suggested filtering addition
func (m *mockSkillStore) Search(_ context.Context, opts agent.SearchSkillsOptions) (*exec.PaginatedResult[agent.SkillMetadata], error) { var items []agent.SkillMetadata for _, s := range m.skills { + if opts.AllowedIDs != nil { + if _, ok := opts.AllowedIDs[s.ID]; !ok { + continue + } + } items = append(items, agent.SkillMetadata{🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/service/frontend/api/v1/agent_skills_test.go` around lines 105 - 135, mockSkillStore.Search currently only paginates; update it to apply SearchSkillsOptions filters before pagination: filter m.skills by AllowedIDs (if opts.AllowedIDs non-empty include only skills whose ID is in that set), by Tags (if opts.Tags non-empty include only skills that contain all requested tags), and by Query (if opts.Query non-empty perform a case-insensitive substring match against skill.Name and skill.Description). After filtering, sort and then use the existing paginator logic (pg := opts.Paginator / exec.DefaultPaginator(), Offset/Limit calculation) and return exec.NewPaginatedResult on the filtered slice; reference mockSkillStore.Search, SearchSkillsOptions, AllowedIDs, Tags, Query, and exec.NewPaginatedResult.internal/service/frontend/server.go (1)
508-530: ExtractautoEnableExampleSkillsto a shared package to avoid duplication.This function (internal/service/frontend/server.go, lines 508–530) is duplicated identically in internal/cmd/context.go. Both load config, deduplicate example skill IDs, and save—this logic should be shared rather than duplicated. Extract to
internal/agentorinternal/persis/fileagentskillto prevent drift.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/service/frontend/server.go` around lines 508 - 530, Move the duplicated autoEnableExampleSkills logic into a single shared package (e.g., internal/agent or internal/persist/fileagentskill) and replace both copies with calls to the new function; specifically extract the implementation that uses agent.ConfigStore (calls configStore.Load, reads/updates cfg.EnabledSkills using fileagentskill.ExampleSkillIDs, then configStore.Save) into a new exported helper (e.g., EnableExampleSkills(ctx, store)), update callers in internal/service/frontend/server.go and internal/cmd/context.go to call that helper, and remove the duplicated function definitions so only the shared implementation remains.ui/src/features/agent/hooks/useAgentChat.ts (1)
261-279:dmin dependency arrays defeatsuseCallbackmemoization.
useDelegateManager()returns a new object literal each render, sodmhas a new identity every cycle. ListingdminuseCallbackdeps (lines 278, 290, 307) meansselectSession,handleClearSession, andreopenDelegateare re-created on every render. This isn't a correctness bug but negates memoization.Destructure the stable callbacks you actually reference instead:
Proposed approach
- const dm = useDelegateManager(); + const dm = useDelegateManager(); + const { + restoreDelegates, resetDelegates, openDelegate, + hasDelegateMessages, setDelegateMessagesForId, + } = dm;Then use those individual (stable) references in the dependency arrays:
- [client, remoteNode, setSessionId, setMessages, setSessionState, dm] + [client, remoteNode, setSessionId, setMessages, setSessionState, restoreDelegates]- [clearSession, dm] + [clearSession, resetDelegates]- [client, remoteNode, dm] + [client, remoteNode, hasDelegateMessages, setDelegateMessagesForId, openDelegate]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/features/agent/hooks/useAgentChat.ts` around lines 261 - 279, The issue is that listing the whole dm object from useDelegateManager in dependency arrays defeats useCallback memoization because dm is re-created each render; fix by destructuring the specific stable methods you call from dm (e.g., const { restoreDelegates, clearDelegates, reopenDelegate } = dm) and then use those method references in the dependency arrays of selectSession, handleClearSession, and reopenDelegate instead of dm; update each useCallback dependency list to include only the stable functions (and other true deps) and remove dm.internal/agent/session.go (1)
440-473: Consider guardingsm.enabledSkills/sm.skillStoreaccess.
createLoopreadssm.enabledSkillsandsm.skillStoreoutside the lock (the mutex is released inensureLoopbefore callingcreateLoop). These fields are set once inNewSessionManagerand never mutated, so this is safe today. If that invariant ever changes, this becomes a data race. A brief comment documenting immutability would help future readers.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/agent/session.go` around lines 440 - 473, The createLoop function reads sm.enabledSkills and sm.skillStore without holding the session mutex (ensureLoop releases the lock before calling createLoop), so add a short explicit comment near the top of createLoop (or on the SessionManager fields) stating that enabledSkills and skillStore are immutable after NewSessionManager (or otherwise never mutated) to document the invariant; alternatively, if you prefer stronger safety, capture copies of sm.enabledSkills and sm.skillStore into local variables while holding the lock in ensureLoop before calling createLoop (identify the fields by name: sm.enabledSkills, sm.skillStore and the functions createLoop and ensureLoop).internal/agent/api.go (1)
440-444: Direct field assignment forregistrybreaks encapsulation but is justified.
mgr.registry = &sessionRegistry{sessions: &a.sessions, parent: mgr}sets the unexported field post-construction to resolve the circular dependency (registry needsmgr,mgrconfig accepts registry). This pattern is used in bothCreateSessionandreactivateSession. It's safe since the field isn't read untilAcceptUserMessage, but a brief inline comment explaining why it's set post-construction would help.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/agent/api.go` around lines 440 - 444, Add a brief inline comment explaining why mgr.registry is set directly after NewSessionManager — note that the direct assignment mgr.registry = &sessionRegistry{sessions: &a.sessions, parent: mgr} is used to break a circular dependency between sessionRegistry and session manager and is safe because the registry field isn't accessed until AcceptUserMessage; add this comment next to the assignment in both CreateSession and reactivateSession so future readers understand the post-construction assignment intent.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1689 +/- ##
==========================================
- Coverage 70.12% 69.93% -0.20%
==========================================
Files 365 369 +4
Lines 40320 40769 +449
==========================================
+ Hits 28276 28511 +235
- Misses 9796 9897 +101
- Partials 2248 2361 +113
... and 9 files with indirect coverage changes Continue to review full report in Codecov by Sentry.
🚀 New features to boost your workflow:
|
Summary by CodeRabbit
New Features
Enhancements