sidequestion

package
v0.3.15 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 7, 2026 License: AGPL-3.0 Imports: 14 Imported by: 0

README

ARTEX /btw

普通聊天、任务 MainAgent 和当前任务自己的 Worker 支持独立旁路提问。主输入框输入 /btw 问题 即可提交,空 /btw 或“旁路提问”按钮打开历史。桌面使用可调整宽度的侧栏,移动端使用 Drawer。

旁路回答根据提交时的 Agent 上下文快照生成,支持流式显示、追问、停止和清空。关闭面板、刷新页面或断开 SSE 都不会取消模型请求。停止只影响当前旁路;清空会取消旁路并删除旁路历史,同时保留主上下文快照。

实现边界

沿用 Go、norma v0.3.7、Next.js、现有 Markdown / ResizablePanel / Drawer / AlertDialog 组件;没有修改 norma 源码或为旁路添加依赖。Planner、继承自其他任务的 Worker、工具型子任务升级不在本次范围内。

flowchart LR
    A[主 Agent QueryDeps] --> B[实际 Provider 绑定]
    B --> C[不可变结构化快照]
    B --> D[主 Agent 正常工具循环]
    C --> E[(PostgreSQL 最新快照)]
    E --> F[快照 + 最近成功旁路问答 + 问题]
    F --> G[SideQuestionService 单次 Provider 请求]
    G --> H[(独立旁路历史和用量)]
    H --> I[累计回答 SSE / 旁路面板]
  • capture.go 只在 Options.Deps.CallModel / CallModelSync 标记主循环请求。Provider 装饰器位于具体模型内部、路由池外层选择之后,因此记录实际选中的模型;压缩和摘要请求不覆盖快照。
  • 请求开始、完整模型回复、运行终态发布快照。正在生成的半段回复不发布;工具调用通过 norma 的 MessagesForAPI 保持配对,工具结果在下一次主模型请求或运行终态进入快照。流式中止保留上一个有效边界。
  • 快照通过 JSON 深拷贝保留结构化消息、系统提示、工具定义及生成参数。模型推理不持有快照锁或数据库事务。
  • SideQuestionService 调用具体 Provider;必要时先生成旁路摘要,最终回答仅在首次上下文超限且尚未输出文本/工具调用时允许缩减后重试一次。不创建 agent session,不接入工具执行器、主 transcript、活动流或任务图,也不经过任务模型切换链。回答保留工具定义是为兼容既有结构化工具上下文;摘要请求不提供工具。新返回的工具调用没有执行路径。
  • 每个父会话一个运行请求,单个服务进程最多四个,单次超时 120 秒。旁路使用服务生命周期下的独立取消上下文。
  • 旁路请求保留模型配置引用和非敏感身份摘要;请求时从现有配置取得凭据。配置被删除,或模型、协议、地址等身份字段变化,要求先运行主 Agent 更新快照。测试不会改变产品默认模型。

持久化和恢复

db/schema.sql 自动创建 side_question_sessions 和 side_question_requests。前者保存父资源、最新快照、运行编号、版本和清理版本;后者保存问题、累计回答、状态、模型、快照时间、用量、事件序号和分页序号。

父会话键使用 conversation ID,或 task ID + exploration ID + intent ID。Worker 不使用可复用的执行槽位命名。

快照按父会话合并写入,每 250 ms 刷新一次,数据库比较 (run_id, version) 防止旧版本覆盖。旁路提交前再次保存选定快照。成功保存后释放内存中的大快照;失败时保留待写版本。回答累计内容在流式事件到来时最多每 250 ms 写入一次,终态立即保存并在数据库错误时有限重试。

服务启动将遗留 running 请求标记为 interrupted,保留已落库的部分回答和用量,不自动重放请求。最近成功保存的上下文可直接用于下一次提问。旧会话没有快照时要求先运行主 Agent,不从 UI 活动记录重建上下文。

清空操作递增清理版本并删除请求;条件更新阻止迟到的回调重新写回。物理父资源删除依靠外键级联,Worker 逻辑删除在同一事务中删除旁路数据,并拒绝后续迟到快照。任务归档先阻止新请求、等待主流程停止、取消并等待旁路落库;归档格式为 v3,同时兼容不含旁路表的 v1/v2。

历史完整保存,按序号游标每页最多返回 20 条。模型请求最多回放最近 20 组成功问答原文,同时按 token 预算限制回放量;较旧问答维护独立滚动摘要。主上下文超预算时仅摘要旁路副本的旧部分,保留近期结构化工具调用与结果。摘要、准备进度与用量一起纳入旁路的并发、取消和 120 秒超时限制。详见 上下文预算与开源参考。

HTTP 契约

以下路径作为 {parent},沿用现有认证及资源校验:

  • /api/conversations/{id}
  • /api/tasks/{id}/chat
  • /api/tasks/{id}/intents/{iid}
请求 返回及行为
GET {parent}/side-questions?before={ordinal} items 按新到旧排列、独立 current 运行状态、snapshot 元信息、next_cursor;游标为 0 表示最新页 / 无下一页
POST {parent}/side-questions JSON { "question": "…", "client_request_id": "UUID" };新请求返回 202 及请求对象;相同 ID 和问题返回既有对象 200
DELETE {parent}/side-questions 取消并清空当前父会话的旁路问答
GET /api/side-questions/{requestID}/events snapshot SSE 事件,id 为递增序号,data 为完整累计请求对象;清空时发送 cleared
POST /api/side-questions/{requestID}/cancel 显式取消;终态可从历史或 SSE 读取

问题上限 4000 字符。无快照、模型配置变化、同一父会话忙或幂等 ID 冲突返回 409;全局并发上限返回 429。每次 SSE 连接都先发送累计状态,不依赖客户端之前收到的文本片段。前端按请求 ID + 序号合并,并在切换父会话、清空时废弃旧回调。

验证与参考

自动化检查、实际模型使用及已知限制见 VALIDATION.md。

独立请求参考 Grok CLI side-question.ts(固定提交),运行隔离参考 OpenCode(固定提交)。ARTEX 的上下文使用 norma 的结构化消息,未采用从前端日志拼接文本的方式。

Documentation

Overview

Package sidequestion captures immutable main-agent checkpoints. It never owns an agent session or a tool executor.

Index

Constants

View Source
const DefaultOutputTokens = 8192
View Source
const MaxRecentExchanges = 20

Variables

View Source
var ErrContextBudget = errors.New("旁路上下文压缩后仍超过模型预算,请缩小问题范围或调整模型上下文配置")

Functions

func Attach

func Attach(ctx context.Context, parent Parent, deps harness.QueryDeps, provider llm.Provider) (context.Context, harness.QueryDeps)

Attach uses QueryDeps rather than a global provider hook so compaction and other auxiliary completions cannot replace the main conversation checkpoint.

func Bind

func Bind(inner llm.Provider, model Model) llm.Provider

Bind belongs immediately around a concrete provider, inside any routing pool.

func BuildRequest

func BuildRequest(s Snapshot, history []Exchange, question string) (llm.CompletionRequest, error)

func EstimateInputTokens

func EstimateInputTokens(req llm.CompletionRequest) int

EstimateInputTokens follows norma's byte-based block estimate with its 4/3 safety factor. Include system/schema and framing costs too; JSON characters are not tokens (and marshaling HTML can add many non-semantic escapes).

func Finish

func Finish(ctx context.Context, messages []llm.Message)

Finish adds tool results that landed after the final model request. Norma's first API message is its host reminder, absent from Terminal.Messages.

func WithPublisher

func WithPublisher(ctx context.Context, publish Publisher) context.Context

Types

type Answer

type Answer struct {
	Text    string
	Usage   llm.Usage
	ToolUse bool
}

type Capture

type Capture struct {
	// contains filtered or unexported fields
}

type ContextInfo

type ContextInfo struct {
	Phase                string `json:"phase,omitempty"`
	RecentExchanges      int    `json:"recent_exchanges"`
	HistorySummarized    bool   `json:"history_summarized"`
	SnapshotSummarized   bool   `json:"snapshot_summarized"`
	EstimatedInputTokens int    `json:"estimated_input_tokens,omitempty"`
	InputBudget          int    `json:"input_budget,omitempty"`
	OutputTokens         int    `json:"output_tokens,omitempty"`
	OverflowRetried      bool   `json:"overflow_retried,omitempty"`
}

type ContextOptions

type ContextOptions struct{ OutputTokens int }

type Exchange

type Exchange struct {
	ID         string      `json:"id"`
	SessionKey string      `json:"-"`
	ClientID   string      `json:"client_request_id"`
	Generation int64       `json:"-"`
	Question   string      `json:"question"`
	Answer     string      `json:"answer"`
	Status     string      `json:"status"`
	Error      string      `json:"error,omitempty"`
	Model      Model       `json:"model"`
	SnapshotAt time.Time   `json:"snapshot_at"`
	CreatedAt  time.Time   `json:"created_at"`
	Sequence   int64       `json:"sequence"`
	Usage      llm.Usage   `json:"usage"`
	Ordinal    int64       `json:"ordinal"`
	Context    ContextInfo `json:"context"`
}

func (Exchange) Running

func (e Exchange) Running() bool

type Memory

type Memory struct {
	History         string `json:"history,omitempty"`
	Through         int64  `json:"through,omitempty"`
	SnapshotKey     string `json:"snapshot_key,omitempty"`
	SnapshotSummary string `json:"snapshot_summary,omitempty"`
	TailStart       int    `json:"tail_start,omitempty"`
}

Memory is independent of the main snapshot. Through is a persisted ordinal, not an array offset; restart, pagination and failed requests cannot shift it.

type Model

type Model struct {
	ProfileID    int64  `json:"profile_id"`
	Name         string `json:"name"`
	Format       string `json:"format"`
	Model        string `json:"model"`
	Identity     string `json:"identity"`
	Streaming    bool   `json:"streaming"`
	WindowTokens int    `json:"window_tokens"`
}

Model contains only configuration identity, never credentials or proxy URLs.

type Parent

type Parent struct {
	ConversationID int64 `json:"conversation_id,omitempty"`
	TaskID         int64 `json:"task_id,omitempty"`
	ExplorationID  int64 `json:"exploration_id,omitempty"`
	IntentID       int64 `json:"intent_id,omitempty"`
}

func (Parent) Key

func (p Parent) Key() string

type Publisher

type Publisher func(Snapshot)

type Replay

type Replay struct {
	Memory Memory
	Load   func(context.Context, int64) ([]Exchange, error)
	Save   func(context.Context, Memory) error
}

Load returns ascending completed exchanges after the cursor, in bounded pages, restricted to ordinals before this request. Save must reject writes after a clear/delete/cancel using the admitted request's generation.

type SideQuestionService

type SideQuestionService struct{ Provider llm.Provider }

SideQuestionService has no harness, tool executor, transcript writer or model failover chain. Answer is one completion; Respond adds bounded preparation and at most one context-overflow recovery around that completion.

func (SideQuestionService) Answer

func (s SideQuestionService) Answer(ctx context.Context, req llm.CompletionRequest, streaming bool, update func(Answer)) (out Answer, err error)

func (SideQuestionService) Respond

func (s SideQuestionService) Respond(ctx context.Context, snapshot Snapshot, question string, replay Replay, options ContextOptions, update func(Answer, ContextInfo)) (out Answer, info ContextInfo, err error)

Respond owns preparation and at most one overflow recovery. No tools, main transcript or model-failover chain are introduced by the summary calls.

type Snapshot

type Snapshot struct {
	Parent     Parent                `json:"parent"`
	RunID      int64                 `json:"run_id"`
	Version    int64                 `json:"version"`
	CapturedAt time.Time             `json:"captured_at"`
	Model      Model                 `json:"model"`
	Request    llm.CompletionRequest `json:"request"`
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL