Files
yawan f7fc992f57
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
2026-09-22 16:19:06 +08:00

427 lines
20 KiB
Go

package types
import (
"context"
"database/sql/driver"
"encoding/json"
"strings"
"time"
)
// DefaultMaxContextTokens is the context window assumed for a model that does
// not declare one. 200k matches typical windows on current chat/VLM models
// (GPT-4.1, Claude, Gemini, Qwen, etc.).
//
// It is still below the largest windows on the market. Guessing high is
// the dangerous direction: compaction never fires, and the first sign of
// trouble is the provider rejecting the request. Guessing low only costs some
// history that would have fit.
const DefaultMaxContextTokens = 200000
// AgentMaxContextTokens picks the context window for one agent run. An
// explicit agent setting wins, then whatever the model declares, then the
// assumed default.
func AgentMaxContextTokens(configured, modelContextWindow int) int {
if configured > 0 {
return configured
}
if modelContextWindow > 0 {
return modelContextWindow
}
return DefaultMaxContextTokens
}
// DefaultSmartReasoningMaxCompletionTokens is the per-round budget when the
// agent cannot emit write_sandbox_file / edit_sandbox_file. 4096 matches
// typical OpenAI-compatible provider defaults and is enough for ordinary
// tool-call JSON.
const DefaultSmartReasoningMaxCompletionTokens = 4096
// DefaultAgentMaxCompletionTokens is the per-round budget when the agent
// can write or edit sandbox files. Those tool calls carry the file body in
// JSON; a 4096 cap truncates mid-stream (finish_reason=length).
const DefaultAgentMaxCompletionTokens = 24576
// DefaultQuickAnswerMaxCompletionTokens is the RAG answer budget.
const DefaultQuickAnswerMaxCompletionTokens = 2048
// NeedsSandboxWriteCompletionBudget reports whether this agent may register
// write_sandbox_file / edit_sandbox_file. Those tools follow the bound
// sandbox, not the allowed_tools checklist.
func NeedsSandboxWriteCompletionBudget(sandboxConfigID string) bool {
return strings.TrimSpace(sandboxConfigID) != ""
}
// DefaultMaxCompletionTokens is the unset-config default for one agent:
// quick-answer 2048, smart-reasoning 4096, smart-reasoning with a sandbox 24576.
func DefaultMaxCompletionTokens(agentMode, sandboxConfigID string) int {
if agentMode == AgentModeSmartReasoning {
if NeedsSandboxWriteCompletionBudget(sandboxConfigID) {
return DefaultAgentMaxCompletionTokens
}
return DefaultSmartReasoningMaxCompletionTokens
}
return DefaultQuickAnswerMaxCompletionTokens
}
// AgentRoundMaxCompletionTokens returns the completion-token budget for one
// ReAct LLM round when no sandbox is bound. Zero (unset) uses
// DefaultSmartReasoningMaxCompletionTokens. An explicit configured value is
// honored as-is.
func AgentRoundMaxCompletionTokens(configured int) int {
return AgentRoundMaxCompletionTokensFor(configured, "")
}
// MinSandboxWriteCompletionTokens is the floor applied to an explicitly
// configured per-round budget when a sandbox is bound. write_sandbox_file and
// edit_sandbox_file carry the whole file body inside the tool-call JSON, so a
// cap sized for ordinary chat truncates the arguments mid-string on every
// attempt — the agent then burns its rounds rewriting a file it can never
// finish. Below this the setting is not a preference, it is a deadlock.
const MinSandboxWriteCompletionTokens = 8192
// AgentRoundMaxCompletionTokensFor is AgentRoundMaxCompletionTokens with
// the agent's sandbox: unset + sandbox uses the large write-file budget, and a
// configured value too small to hold a file write is raised to the floor.
func AgentRoundMaxCompletionTokensFor(configured int, sandboxConfigID string) int {
if configured > 0 {
if NeedsSandboxWriteCompletionBudget(sandboxConfigID) &&
configured < MinSandboxWriteCompletionTokens {
return MinSandboxWriteCompletionTokens
}
return configured
}
return DefaultMaxCompletionTokens(AgentModeSmartReasoning, sandboxConfigID)
}
// UnlimitedMaxIterations is the stored MaxIterations value meaning the ReAct
// loop has no round cap. Zero still means "unset, apply default" so agents
// that omit the field keep their current behaviour.
const UnlimitedMaxIterations = -1
// AgentConfig represents the full agent configuration (used at tenant level and runtime)
// This includes all configuration parameters for agent execution
type AgentConfig struct {
// MaxIterations caps ReAct rounds. Zero is unset (filled with a default);
// a negative value is unlimited — the loop runs until the model stops,
// the user cancels, or another guard fires. See UnlimitedMaxIterations.
MaxIterations int `json:"max_iterations"`
AllowedTools []string `json:"allowed_tools"` // List of allowed tool names
Temperature float64 `json:"temperature"` // LLM temperature for agent
KnowledgeBases []string `json:"knowledge_bases"` // Accessible knowledge base IDs
KnowledgeIDs []string `json:"knowledge_ids"` // Accessible knowledge IDs (individual documents)
SystemPrompt string `json:"system_prompt,omitempty"` // Unified system prompt (uses web_search_status placeholder for dynamic behavior)
InstructionSkillPrompt string `json:"-"` // Resolved runtime-only Markdown rules, appended to the base prompt.
// Deprecated: Use SystemPrompt instead. Kept for backward compatibility during migration.
SystemPromptWebEnabled string `json:"system_prompt_web_enabled,omitempty"` // Deprecated: Custom prompt when web search is enabled
SystemPromptWebDisabled string `json:"system_prompt_web_disabled,omitempty"` // Deprecated: Custom prompt when web search is disabled
UseCustomSystemPrompt bool `json:"use_custom_system_prompt"` // Whether to use custom system prompt instead of default
WebSearchEnabled bool `json:"web_search_enabled"` // Whether web search tool is enabled
WebSearchMaxResults int `json:"web_search_max_results"` // Maximum number of web search results (default: 5)
WebSearchProviderID string `json:"web_search_provider_id,omitempty"` // WebSearchProviderEntity ID (resolved from agent config)
MultiTurnEnabled bool `json:"multi_turn_enabled"` // Whether multi-turn conversation is enabled
HistoryTurns int `json:"history_turns"` // Number of history turns to keep in context
MemoryEnabled *bool `json:"memory_enabled,omitempty"` // nil inherits workspace
SearchTargets SearchTargets `json:"-"` // Pre-computed unified search targets (runtime only)
// MCP service selection
MCPSelectionMode string `json:"mcp_selection_mode"` // MCP selection mode: "all", "selected", "none"
MCPServices []string `json:"mcp_services"` // Selected MCP service IDs (when mode is "selected")
// MCPAuthWaitTimeout is how many seconds an agent run waits for
// in-conversation OAuth authorization before skipping. <=0 falls back to
// the gate's configured timeout. The wait is always bounded (no leak).
MCPAuthWaitTimeout int `json:"mcp_auth_wait_timeout,omitempty"`
// Whether to enable thinking mode (for models that support extended thinking)
Thinking *bool `json:"thinking"`
// Whether final answers include knowledge/web source citations. Nil defaults to true.
CitationEnabled *bool `json:"citation_enabled"`
// Whether to retrieve knowledge base only when explicitly mentioned with @ (default: false)
RetrieveKBOnlyWhenMentioned bool `json:"retrieve_kb_only_when_mentioned"`
// Whether to retain retrieval history (like wiki_read_page results) across turns (default: false)
RetainRetrievalHistory bool `json:"retain_retrieval_history"`
// Skills configuration (Progressive Disclosure pattern)
SkillsEnabled bool `json:"skills_enabled"` // Whether skills are enabled (default: false)
SkillDirs []string `json:"skill_dirs"` // Directories to search for skills
AllowedSkills []string `json:"allowed_skills"` // Skill names whitelist (empty = allow all)
// Runtime-only fields (not persisted)
VLMModelID string `json:"-"` // VLM model ID for tool result image analysis (set from CustomAgent config)
SandboxConfigID string `json:"-"` // Workspace sandbox config ID for skill execution (set from CustomAgent config)
// TenantSkills are the skills installed into the selected sandbox config's
// snapshot image, already narrowed to the ones this run can actually
// invoke. Runtime only: it is derived per turn from the config the agent
// selected, never stored on the agent record.
TenantSkills []*TenantSkillEntity `json:"-"`
// Per-request @mention pins (runtime only; injected as <must_use> in the user message).
PinnedMCPServiceIDs []string `json:"-"`
PinnedSkillNames []string `json:"-"`
// SharedAgentReadOnly prevents a shared agent from mutating resources in
// its source workspace. It is set from the verified share relation, never
// inferred from a client-provided tenant ID.
SharedAgentReadOnly bool `json:"-"`
// LLM call timeout in seconds (default: 120). Controls the maximum time for a single LLM call.
LLMCallTimeout int `json:"llm_call_timeout,omitempty"`
// Maximum completion tokens for each ReAct LLM round. Zero means
// DefaultMaxCompletionTokens for this agent's mode and sandbox.
// Explicit values are sent as-is.
MaxCompletionTokens int `json:"max_completion_tokens,omitempty"`
// Maximum character length for tool output (default: 16000).
// Outputs exceeding this limit are truncated with head + tail preservation.
MaxToolOutputChars int `json:"max_tool_output_chars,omitempty"`
// Maximum context window tokens for the agent. Zero means "use the
// model's context_window, or DefaultMaxContextTokens (200000)".
MaxContextTokens int `json:"max_context_tokens,omitempty"`
// How much recent conversation a compaction keeps verbatim. Zero means
// compaction.DefaultKeepRecentTokens, scaled down on small windows. This
// is the knob that decides how often compaction runs: whatever the context
// grew to, it comes out of a compaction at roughly this size.
CompactionKeepRecentTokens int `json:"compaction_keep_recent_tokens,omitempty"`
// Whether to execute independent tool calls in parallel (default: false).
// When enabled and the LLM returns multiple tool calls, they run concurrently via errgroup.
ParallelToolCalls bool `json:"parallel_tool_calls,omitempty"`
// skillInstallMode routes this run's shell_exec to the privileged
// install-mode executor (root, skills image root writable). It is
// unexported on purpose: JSON cannot reach it, so no stored agent record
// and no API payload can request the privilege, and no other package can
// assign it. EnableSkillInstallMode is the only way in and it refuses
// every agent except the built-in skill installer.
skillInstallMode bool
// skillInstallDir is the one skill directory this install owns. The skill
// file tools are scoped to it, so an installer cannot write a neighbouring
// skill in the shared image. Unexported for the same reason as
// skillInstallMode: the scope of a privilege must not be settable by
// anything a tenant can store or send.
skillInstallDir string
}
// EnableSkillInstallMode grants install-mode shell execution to the built-in
// skill installer agent and to nothing else, scoped to the skill directory it
// was started for. The agent ID is checked here rather than at the call site
// so there is exactly one place to audit.
//
// The privilege and its scope are granted together: a caller cannot obtain the
// root shell without naming the directory the file tools will be confined to.
// skillDir is stored verbatim and validated where the tools are constructed —
// types cannot import the sandbox package that owns the path rules.
func (c *AgentConfig) EnableSkillInstallMode(agentID, skillDir string) {
if c == nil || agentID != BuiltinSkillInstallerID {
return
}
c.skillInstallMode = true
c.skillInstallDir = skillDir
}
// SkillInstallMode reports whether this run may use the privileged
// install-mode shell.
func (c *AgentConfig) SkillInstallMode() bool {
return c != nil && c.skillInstallMode
}
// SkillInstallDir is the skill directory this install may write, or "" when
// this run is not an install.
func (c *AgentConfig) SkillInstallDir() string {
if c == nil || !c.skillInstallMode {
return ""
}
return c.skillInstallDir
}
// UnlimitedIterations reports whether the ReAct loop has no round cap.
func (c *AgentConfig) UnlimitedIterations() bool {
return c != nil && c.MaxIterations < 0
}
// CitationsEnabled preserves citation output for legacy runtime configs that
// predate the setting and therefore have a nil CitationEnabled value.
func (c *AgentConfig) CitationsEnabled() bool {
return c == nil || c.CitationEnabled == nil || *c.CitationEnabled
}
// SessionAgentConfig represents session-level agent configuration
// Sessions only store Enabled and KnowledgeBases; other configs are read from Tenant at runtime
type SessionAgentConfig struct {
AgentModeEnabled bool `json:"agent_mode_enabled"` // Whether agent mode is enabled for this session
WebSearchEnabled bool `json:"web_search_enabled"` // Whether web search is enabled for this session
KnowledgeBases []string `json:"knowledge_bases"` // Accessible knowledge base IDs for this session
KnowledgeIDs []string `json:"knowledge_ids"` // Accessible knowledge IDs (individual documents) for this session
}
// Value implements driver.Valuer interface for AgentConfig
func (c AgentConfig) Value() (driver.Value, error) {
return json.Marshal(c)
}
// Scan implements sql.Scanner interface for AgentConfig
func (c *AgentConfig) Scan(value interface{}) error {
if value == nil {
return nil
}
var b []byte
switch v := value.(type) {
case []byte:
b = v
case string:
b = []byte(v)
default:
return nil
}
return json.Unmarshal(b, c)
}
// Value implements driver.Valuer interface for SessionAgentConfig
func (c SessionAgentConfig) Value() (driver.Value, error) {
return json.Marshal(c)
}
// Scan implements sql.Scanner interface for SessionAgentConfig
func (c *SessionAgentConfig) Scan(value interface{}) error {
if value == nil {
return nil
}
var b []byte
switch v := value.(type) {
case []byte:
b = v
case string:
b = []byte(v)
default:
return nil
}
return json.Unmarshal(b, c)
}
// ResolveSystemPrompt returns the prompt template for the given web search state.
// It uses the unified SystemPrompt field, falling back to deprecated fields for backward compatibility.
func (c *AgentConfig) ResolveSystemPrompt(webSearchEnabled bool) string {
if c == nil {
return ""
}
// First, try the new unified SystemPrompt field
if c.SystemPrompt != "" {
return c.SystemPrompt
}
// Fallback to deprecated fields for backward compatibility
if webSearchEnabled {
if c.SystemPromptWebEnabled != "" {
return c.SystemPromptWebEnabled
}
} else {
if c.SystemPromptWebDisabled != "" {
return c.SystemPromptWebDisabled
}
}
return ""
}
// Tool defines the interface that all agent tools must implement
type Tool interface {
// Name returns the unique identifier for this tool
Name() string
// Description returns a human-readable description of what the tool does
Description() string
// Parameters returns the JSON Schema for the tool's parameters
Parameters() json.RawMessage
// Execute runs the tool with the given arguments
Execute(ctx context.Context, args json.RawMessage) (*ToolResult, error)
}
// Cleanable is an optional interface that tools can implement to release resources.
// Tools implementing this interface will have their Cleanup method called during
// registry cleanup (e.g., at the end of an agent session).
type Cleanable interface {
Cleanup(ctx context.Context)
}
// ToolResult represents the result of a tool execution
type ToolResult struct {
Success bool `json:"success"` // Whether the tool executed successfully
Output string `json:"output"` // Human-readable output
Data map[string]interface{} `json:"data,omitempty"` // Structured data for programmatic use
Error string `json:"error,omitempty"` // Error message if execution failed
Images []string `json:"images,omitempty"` // Base64 data URIs from tool (e.g. MCP image content)
}
// ToolCall represents a single tool invocation within an agent step
type ToolCall struct {
ID string `json:"id"` // Function call ID from LLM
Name string `json:"name"` // Tool name
Args map[string]interface{} `json:"args"` // Tool arguments
Result *ToolResult `json:"result"` // Execution result (contains Output)
Reflection string `json:"reflection,omitempty"` // Agent's reflection on this tool call result (if enabled)
Duration int64 `json:"duration"` // Execution time in milliseconds
ProviderMetadata ToolCallMetadata `json:"provider_metadata,omitempty"` // Provider-specific tool-call state for replay
}
// PipelineToolCallIDPrefix marks a persisted tool call the model never made.
// The fast-answer (KnowledgeQA) pipeline records its retrieval stages as tool
// calls so a reloaded conversation can redraw the same timeline it showed while
// streaming. History replay must skip them: asking the model to account for
// calls it never issued, against tools it may not even have, breaks the
// request protocol.
const PipelineToolCallIDPrefix = "ragpipe-"
// IsPipelineToolCallID reports whether a tool call was synthesized by the
// fast-answer pipeline rather than requested by the model.
func IsPipelineToolCallID(id string) bool {
return strings.HasPrefix(id, PipelineToolCallIDPrefix)
}
// AgentStep represents one iteration of the ReAct loop
type AgentStep struct {
Iteration int `json:"iteration"` // Iteration number (0-indexed)
Thought string `json:"thought"` // LLM's reasoning/thinking (Think phase)
// ReasoningContent stores the OpenAI-protocol reasoning_content emitted by the
// model in this round. Persisted on AgentStep so cross-turn replay can put it
// back on the assistant message — required by MiMo / DeepSeek V3.2+ thinking
// mode, ignored by providers that don't recognize the field.
ReasoningContent string `json:"reasoning_content,omitempty"`
ToolCalls []ToolCall `json:"tool_calls"` // Tools called in this step (Act phase)
Timestamp time.Time `json:"timestamp"` // When this step occurred
}
// GetObservations returns observations from all tool calls in this step
// This is a convenience method to maintain backward compatibility
func (s *AgentStep) GetObservations() []string {
observations := make([]string, 0, len(s.ToolCalls))
for _, tc := range s.ToolCalls {
if tc.Result != nil && tc.Result.Output != "" {
observations = append(observations, tc.Result.Output)
}
if tc.Reflection != "" {
observations = append(observations, "Reflection: "+tc.Reflection)
}
}
return observations
}
// AgentState tracks the execution state of an agent across iterations
type AgentState struct {
CurrentRound int `json:"current_round"` // Current round number
RoundSteps []AgentStep `json:"round_steps"` // All steps taken so far in the current round
IsComplete bool `json:"is_complete"` // Whether agent has finished
FinalAnswer string `json:"final_answer"` // The final answer to the query
KnowledgeRefs []*SearchResult `json:"knowledge_refs"` // Collected knowledge references
TurnUsage TokenUsage `json:"turn_usage"` // LLM token usage accumulated across every round of this turn
}
// FunctionDefinition represents a function definition for LLM function calling
type FunctionDefinition struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters json.RawMessage `json:"parameters"`
}