69 lines
3.5 KiB
Swift
69 lines
3.5 KiB
Swift
import AgentKit
|
|
import AgentKitScrubber
|
|
import Foundation
|
|
import TermindApplication
|
|
import TermindDomain
|
|
|
|
@MainActor
|
|
public enum AssistantToolRegistry {
|
|
public static func make(
|
|
conversation: ConversationModel, model: AIModel, provider: ModelProvider,
|
|
skills skillCatalog: AgentSkillCatalog, channel: AgentEventChannel,
|
|
ai: AIConfigurationModel, hosts: AgentHostResolver,
|
|
metrics: any MetricsSnapshotReading, connections: AgentConnectionPool,
|
|
authentication: AgentSSHAuthenticationRegistry, background: AgentBackgroundCommands
|
|
) -> AgentToolRegistry {
|
|
let native =
|
|
ai.webSearch.isEnabled && ai.webSearch.prefersBuiltIn && provider.supportsNativeWebSearch(model: model)
|
|
let localSearch: ScrubberWebClient? = ai.webSearch.isEnabled && !native ? ScrubberWebClient() : nil
|
|
var registry = AgentToolRegistry([AnyAgentTool]())
|
|
if model.abilities.contains(.toolCall) {
|
|
// One workspace per conversation, not per run: a file staged before the
|
|
// user interrupted, or before they typed a follow-up, is still there for
|
|
// the run that answers them. Isolation comes from the scope directory
|
|
// being keyed by conversation, so one chat can never read what another
|
|
// pulled off a host.
|
|
let scratch = AgentScratchWorkspace(
|
|
conversationID: conversation.id,
|
|
applicationName: TermindAgentPolicy.scratchDirectoryName
|
|
)
|
|
// One client for both, rather than two default-constructed ones: `fetch`
|
|
// and `scratch_fetch` read the same web the same way and differ only in
|
|
// where the page ends up.
|
|
let web = ScrubberWebClient()
|
|
// Both write through the one ordered channel the rest of the run
|
|
// uses, rather than reaching into the conversation directly: a tool
|
|
// that touched `@Observable` state from a task group would be
|
|
// mutating the UI from off the main actor.
|
|
let servers = ai.usableMCPServers.map { server in
|
|
(server: server, bearerToken: ai.secret(for: server.credentialRef))
|
|
}
|
|
// Local definitions are registered once per catalog — the runtime's
|
|
// built-in groups, then ours; discovered MCP definitions join the
|
|
// same resolved registry after both, so neither a Termind name nor a
|
|
// remote name can shadow a built-in.
|
|
registry = AgentToolCatalog.registry(
|
|
builtIn: AgentBuiltInToolConfiguration(
|
|
workspace: scratch,
|
|
web: web,
|
|
search: localSearch,
|
|
secretConsumerTools: [TermindAgentPolicy.sshExecToolName],
|
|
tasks: AgentTaskList { channel.emit(.tasksUpdated($0)) },
|
|
plans: AgentPlanRecorder { channel.emit(.planPresented($0)) },
|
|
skills: skillCatalog,
|
|
mcpServers: servers,
|
|
mcpClient: MCPClient(clientInfo: TermindAgentPolicy.mcpClientInfo)
|
|
),
|
|
additional: TermindAgentTools.tools(
|
|
in: TermindAgentToolEnvironment(
|
|
hosts: hosts, metrics: metrics, connections: connections,
|
|
authentication: authentication,
|
|
background: background,
|
|
workspace: scratch
|
|
))
|
|
)
|
|
}
|
|
return registry
|
|
}
|
|
}
|