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

306 lines
12 KiB
Go

package types
import (
"database/sql/driver"
"encoding/json"
)
// PromptCacheStatus distinguishes a real cache miss from providers that do not
// expose prompt-cache accounting. Treating both as zero makes fleet-level hit
// rate dashboards misleading.
type PromptCacheStatus string
const (
PromptCacheStatusUnsupported PromptCacheStatus = "unsupported"
PromptCacheStatusUnreported PromptCacheStatus = "unreported"
PromptCacheStatusMiss PromptCacheStatus = "miss"
PromptCacheStatusHit PromptCacheStatus = "hit"
)
// TokenUsage holds token consumption statistics returned by the model API.
type TokenUsage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
// CachedTokens is the legacy alias for CacheReadTokens. It remains on the
// wire for compatibility with existing API consumers.
CachedTokens int `json:"cached_tokens,omitempty"`
CacheReadTokens int `json:"cache_read_tokens,omitempty"`
CacheWriteTokens int `json:"cache_write_tokens,omitempty"`
CacheMissTokens int `json:"cache_miss_tokens,omitempty"`
CacheReported bool `json:"cache_reported"`
CacheStatus PromptCacheStatus `json:"cache_status,omitempty"`
}
// SetPromptCacheUsage normalizes provider-specific cache counters into the
// shared usage model. promptTokens remains the provider's total input-token
// count; read/write/miss are descriptive subsets and must not be added to it.
func (u *TokenUsage) SetPromptCacheUsage(read, write, miss int, reported bool) {
if u == nil {
return
}
if read < 0 {
read = 0
}
if write < 0 {
write = 0
}
if miss < 0 {
miss = 0
}
u.CachedTokens = read
u.CacheReadTokens = read
u.CacheWriteTokens = write
u.CacheMissTokens = miss
u.CacheReported = reported
switch {
case !reported:
u.CacheStatus = PromptCacheStatusUnreported
case read > 0:
u.CacheStatus = PromptCacheStatusHit
default:
u.CacheStatus = PromptCacheStatusMiss
}
}
// MarkPromptCacheUnsupported marks a provider/model path that cannot report
// provider-side prompt-cache usage.
func (u *TokenUsage) MarkPromptCacheUnsupported() {
if u == nil {
return
}
u.SetPromptCacheUsage(0, 0, 0, false)
u.CacheStatus = PromptCacheStatusUnsupported
}
// Accumulate adds another call's usage into u, preserving the subset
// semantics: prompt/completion/total and every cache counter sum
// independently (cache counters stay subsets of the prompt count and are
// never folded into it). CacheReported ORs, and the cache status is
// recomputed from the combined counters so a single cache hit anywhere in
// the accumulated calls reads as a hit.
func (u *TokenUsage) Accumulate(other TokenUsage) {
if u == nil {
return
}
u.PromptTokens += other.PromptTokens
u.CompletionTokens += other.CompletionTokens
u.TotalTokens += other.TotalTokens
u.CachedTokens += other.CachedTokens
u.CacheReadTokens += other.CacheReadTokens
u.CacheWriteTokens += other.CacheWriteTokens
u.CacheMissTokens += other.CacheMissTokens
u.CacheReported = u.CacheReported || other.CacheReported
switch {
case !u.CacheReported:
u.CacheStatus = mergeUnreportedCacheStatus(u.CacheStatus, other.CacheStatus)
case u.CacheReadTokens > 0:
u.CacheStatus = PromptCacheStatusHit
default:
u.CacheStatus = PromptCacheStatusMiss
}
}
// mergeUnreportedCacheStatus folds the statuses of never-reported usage.
// "unsupported" survives only while every accumulated call was itself
// classified unsupported — the first accumulation adopts the incoming
// classification, and any later call that is not known-unsupported (an
// unreported or unclassified one) degrades the aggregate to unreported.
func mergeUnreportedCacheStatus(accumulated, incoming PromptCacheStatus) PromptCacheStatus {
if accumulated == "" {
if incoming == "" {
return PromptCacheStatusUnreported
}
return incoming
}
if accumulated == PromptCacheStatusUnsupported && incoming == PromptCacheStatusUnsupported {
return PromptCacheStatusUnsupported
}
return PromptCacheStatusUnreported
}
// PromptCacheHitRate returns cache-read tokens as a percentage of the
// provider's prompt total. Zero when the prompt is empty. A ReAct turn that
// reuses the system prefix therefore reads as a high hit rather than a miss.
func (u TokenUsage) PromptCacheHitRate() float64 {
if u.PromptTokens <= 0 {
return 0
}
return float64(u.CacheReadTokens) / float64(u.PromptTokens) * 100
}
// Value persists the usage as a jsonb column (assistant messages carry the
// turn's aggregate); nil writes SQL NULL. Mirrors the nullable-pointer
// pattern APIPrincipalConfig uses.
func (u *TokenUsage) Value() (driver.Value, error) {
if u == nil {
return nil, nil
}
return json.Marshal(u)
}
// Scan restores a jsonb usage column; NULL leaves the receiver zero-valued.
func (u *TokenUsage) 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, u)
}
// LLMToolCall represents a function/tool call from the LLM
type LLMToolCall struct {
ID string `json:"id"`
Type string `json:"type"` // "function"
Function FunctionCall `json:"function"`
ProviderMetadata ToolCallMetadata `json:"provider_metadata,omitempty"`
// ModelArguments and the resolution fields are request-local observability
// state. ModelArguments preserves the exact JSON emitted by the model while
// Function.Arguments is decoded to durable application identifiers before
// tool execution. These fields must never be sent back to a provider or
// persisted in chat history.
ModelArguments string `json:"-"`
ArgumentResolution string `json:"-"`
UnresolvedHandles []string `json:"-"`
}
// ToolCallMetadata carries provider-specific tool-call state that must round-trip
// with the assistant tool call, without teaching core agent code vendor fields.
type ToolCallMetadata map[string]json.RawMessage
// FunctionCall represents the function details
type FunctionCall struct {
Name string `json:"name"`
Arguments string `json:"arguments"` // JSON string
}
// ChatResponse chat response
type ChatResponse struct {
Content string `json:"content"`
// ReasoningContent 是支持思考链的模型(DeepSeek thinking、小米 MiMo、vLLM reasoning 等)
// 在本轮输出的推理内容。需要在后续多轮请求中原样回传给那些严格校验的供应商。
ReasoningContent string `json:"reasoning_content,omitempty"`
ToolCalls []LLMToolCall `json:"tool_calls,omitempty"`
FinishReason string `json:"finish_reason,omitempty"`
Usage TokenUsage `json:"usage"`
// AnswerStreamed reports whether the user-facing answer text was already
// streamed live to the final-answer UI area during this round (i.e. the
// model answered with plain content). When true, the natural-stop branch
// must only emit the closing
// Done marker for AnswerEventID instead of re-emitting the whole answer —
// otherwise the answer would render twice and "jump" at end of stream.
// Transient, never persisted.
AnswerStreamed bool `json:"-"`
// AnswerEventID is the EventBus event ID under which the live answer
// chunks were streamed, so the natural-stop branch can close the same
// stream with a Done marker. Empty when AnswerStreamed is false.
AnswerEventID string `json:"-"`
}
// Response type
type ResponseType string
const (
// Answer response type
ResponseTypeAnswer ResponseType = "answer"
// References response type
ResponseTypeReferences ResponseType = "references"
// Thinking response type (for agent thought process)
ResponseTypeThinking ResponseType = "thinking"
// Tool call response type (for agent tool invocations)
ResponseTypeToolCall ResponseType = "tool_call"
// Tool result response type (for agent tool results)
ResponseTypeToolResult ResponseType = "tool_result"
// Error response type
ResponseTypeError ResponseType = "error"
// Reflection response type (for agent reflection)
ResponseTypeReflection ResponseType = "reflection"
// Session title response type
ResponseTypeSessionTitle ResponseType = "session_title"
// Agent query response type (query received and processing started)
ResponseTypeAgentQuery ResponseType = "agent_query"
// Complete response type (agent complete)
ResponseTypeComplete ResponseType = "complete"
// ResponseTypeArtifactsPending is sent while skill/sandbox output is being
// copied into persistent storage after the answer has already streamed.
// The UI shows a toolbar placeholder until ResponseTypeComplete carries
// the file list.
ResponseTypeArtifactsPending ResponseType = "artifacts_pending"
// ToolApprovalRequired: MCP tool marked dangerous — UI must collect user approval before execution continues
ResponseTypeToolApprovalRequired ResponseType = "tool_approval_required"
// ToolApprovalResolved: user approved/rejected (or timeout); informational for UI replay
ResponseTypeToolApprovalResolved ResponseType = "tool_approval_resolved"
// MCPOAuthRequired: an OAuth-enabled MCP service was invoked but the user
// has not authorized it — UI must surface an "Authorize" prompt and the
// agent pauses until authorization completes (or the wait times out).
ResponseTypeMCPOAuthRequired ResponseType = "mcp_oauth_required"
// MCPOAuthResolved: authorization completed / timed out / canceled;
// informational for UI replay.
ResponseTypeMCPOAuthResolved ResponseType = "mcp_oauth_resolved"
// MemoryRecalled: the long-term memories injected into this answer, so
// the UI can show and let the user delete what influenced it.
ResponseTypeMemoryRecalled ResponseType = "memory_recalled"
// ResponseTypeContextCompacted is older conversation summarized away to
// fit the context window. Surfaced because it changes what the agent
// remembers — an answer that forgets an earlier instruction is otherwise
// indistinguishable from the model ignoring it.
ResponseTypeContextCompacted ResponseType = "context_compacted"
// ResponseTypeInstallPrompt is the instruction a skill install handed to
// the installer agent. Only the skill install transcript emits this, and
// it emits it first, so replaying the log alone shows what was asked for
// — the console does not have to read the durable prompt row to caption
// the run.
ResponseTypeInstallPrompt ResponseType = "install_prompt"
)
// StreamResponse stream response
// FinishReasonIncomplete marks a stream that broke before the provider sent a
// finish reason (read error, timeout, stall). Whatever content and tool calls
// were collected are a partial response: callers must not treat them as a
// completed turn.
const FinishReasonIncomplete = "incomplete"
type StreamResponse struct {
ID string `json:"id"`
ResponseType ResponseType `json:"response_type"`
Content string `json:"content"`
Done bool `json:"done"`
KnowledgeReferences References `json:"knowledge_references,omitempty"`
SessionID string `json:"session_id,omitempty"`
AssistantMessageID string `json:"assistant_message_id,omitempty"`
ToolCalls []LLMToolCall `json:"tool_calls,omitempty"`
Data map[string]interface{} `json:"data,omitempty"`
Usage *TokenUsage `json:"usage,omitempty"`
FinishReason string `json:"finish_reason,omitempty"`
}
// References references
type References []*SearchResult
// Value implements the driver.Valuer interface, used to convert References to database values
func (c References) Value() (driver.Value, error) {
return json.Marshal(c)
}
// Scan implements the sql.Scanner interface, used to convert database values to References
func (c *References) Scan(value interface{}) error {
if value == nil {
return nil
}
b, ok := value.([]byte)
if !ok {
return nil
}
return json.Unmarshal(b, c)
}