Skip to content
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,22 @@ Around that collaboration model, TiyCode brings together Agent Profiles, workspa

- **AI-first coding collaboration.** TiyCode is designed around the idea that humans express intent through conversation while agents take the lead in execution.
- **Agent Profiles.** Mix models from different providers, tune response style, language, and custom instructions, and switch profiles flexibly for different kinds of work.
- **Persistent goal management.** Define long-running objectives for agents to pursue across multiple turns. An independent Judge subagent evaluates completion against actual file changes, command outputs, and commit history — eliminating self-attestation bias.
- **Custom Agents.** Create purpose-built sub-agents in Settings — each with its own name, system prompt, model tier, and allowed tools — then grant per-profile access and delegate work from the composer.
- **Three-tier model architecture.** Each profile supports a Primary model for core reasoning, an Auxiliary model for helper tasks, and a Lightweight model for fast operations — with automatic fallback chains across tiers.
- **Multi-provider support.** Connect to 13+ LLM providers out of the box — OpenAI, Anthropic, Google, Ollama, xAI, Groq, OpenRouter, DeepSeek, MiniMax, Kimi, and more — or add any OpenAI-compatible endpoint as a custom provider.
- **Workspace-centered execution.** Threads stay grounded in the local workspace and connect naturally to code review, version control, repository inspection, Git worktrees, and terminal workflows.
- **Task-aware execution.** Thread-scoped task boards, plan checkpoints, tool status events, and subagent progress make longer runs easier to follow and review.
- **Persistent goal management.** Set long-running objectives for agents to pursue across multiple turns, with automatic continuation, budget controls, and progress tracking.
- **Real-time execution streaming.** A rich thread stream event system delivers live updates — message deltas, tool calls, requested/active statuses, reasoning steps, subagent progress, and plan updates — all rendered through purpose-built AI Elements components.
- **Rich composer inputs.** Prompt input supports text, file/photo attachments, screenshots, slash command structured argument interpolation (`--key=value`, positional args, `{{placeholder}}` templates), and large-paste handling.
- **Steer & Queue.** While the agent is running, choose to steer the conversation mid-execution or queue a follow-up message for the next round — keeping you in control without interrupting the workflow.
- **Real-time execution streaming.** A rich thread stream event system delivers live updates — message deltas, tool calls, requested/active statuses, reasoning steps, subagent progress, and plan updates — all rendered through purpose-built AI Elements components.
- **Operator-friendly experience.** Slash commands with structured argument parsing, smart conversation titles, context compression controls, commit message generation, external terminal handoff including Ghostty, and compact workbench controls help the product feel fast and practical in day-to-day use.
- **Thread-level elapsed timer.** Track active execution time per thread, excluding pauses, with persistent tracking across sessions.
- **Bilingual interface.** Full i18n coverage with English and Simplified Chinese, switchable at any time.
- **Extensible by design.** Plugins, MCP servers, and Skills are treated as first-class building blocks through the `Extensions Center`.
- **ACP Server support.** TiyCode can run as a headless ACP (Agent Client Protocol) server via `tiycode acp --stdio` or `tiycode acp --http <addr>`, letting external tools and IDE plugins drive the agent runtime through a standard JSON-RPC protocol without the desktop GUI.
- **IM channel gateway.** Connect TiyCode to WeChat or WeCom so you can chat with the agent directly from your messaging app — scan a QR code to log in, send messages and attachments, and receive streaming responses without opening the desktop GUI.
- **Extensible by design.** Plugins, MCP servers, and Skills are treated as first-class building blocks through the `Extensions Center`.
- **Operator-friendly experience.** Slash commands with structured argument parsing, smart conversation titles, context compression controls, commit message generation, external terminal handoff including Ghostty, and compact workbench controls help the product feel fast and practical in day-to-day use.
- **Thread-level elapsed timer.** Track active execution time per thread, excluding pauses, with persistent tracking across sessions.
- **Built-in runtime path.** The main execution flow is `Frontend -> Rust Core -> BuiltInAgentRuntime -> tiycore -> LLM`.
- **Bilingual interface.** Full i18n coverage with English and Simplified Chinese, switchable at any time.

## Tech Stack

Expand Down
12 changes: 6 additions & 6 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,22 @@ TiyCode 面向的是希望以 AI 时代的方式进行编码协作的用户。

- **AI First 的编码协作。** TiyCode 围绕"通过对话表达意图,Agent 全面执行"这一理念来设计产品形态。
- **Agent Profile。** 支持自由组合不同服务商的模型,并可配置回复风格、回复语言、自定义指令等设定,且能在不同 Profile 之间灵活切换。
- **持久化目标管理。** 为 Agent 设置跨轮次的长期目标,由独立的 Judge 验收 Agent 基于实际文件变更、命令输出和提交历史进行完成判定——杜绝"自说自话"的信任缺陷。
- **Custom Agents。** 在设置中心创建专用子 Agent——每个拥有独立的名称、系统提示、模型层级和可用工具——按 Profile 授权后即可从 composer 委派任务。
- **三层模型架构。** 每个 Profile 支持配置 Primary 主力模型、Auxiliary 辅助模型和 Lightweight 轻量模型三个层级,层级之间具备自动回退链路。
- **多服务商接入。** 开箱支持 13+ 家 LLM 服务商 —— OpenAI、Anthropic、Google、Ollama、xAI、Groq、OpenRouter、DeepSeek、MiniMax、Kimi 等,也可将任何 OpenAI 兼容端点作为自定义 Provider 接入。
- **以工作区为中心的执行体验。** 对话线程扎根本地工作区,并与代码审阅、版本控制、仓库状态读取、Git worktree 和 Terminal 工作流自然衔接。
- **面向任务的执行可观测性。** Thread 级任务板、Plan checkpoint、工具状态事件和子 Agent 进度让长任务更容易跟踪和复查。
- **持久化目标管理。** 为 Agent 设置跨轮次的长期目标,支持自动延续、预算控制和进度跟踪
- **实时执行流式推送。** 丰富的 Thread Stream 事件体系支撑实时更新 —— 消息增量、工具调用、requested / active 状态、推理步骤、子 Agent 进度与计划更新
- **更丰富的输入能力。** Prompt 输入支持文本、文件 / 图片附件、截图、Slash Command 结构化参数插值(`--key=value`、位置参数、`{{placeholder}}` 模板变量)以及大段文本粘贴处理。
- **Steer 与 Queue。** Agent 运行中可选择「引导」即时插入消息调整方向,或「排队」将消息留待当前运行结束后再发起下一轮——无需中断工作流即可保持掌控。
- **实时执行流式推送。** 丰富的 Thread Stream 事件体系支撑实时更新 —— 消息增量、工具调用、requested / active 状态、推理步骤、子 Agent 进度与计划更新。
- **更友好的日常体验。** 支持结构化参数解析的 Slash Command、智能会话标题、上下文压缩、Commit Message 生成、包含 Ghostty 在内的外部终端衔接以及紧凑工作台控件,让协作过程更顺手、更连贯。
- **线程级别耗时计时器。** 跟踪每个线程的活跃执行时间,排除暂停时间,并支持跨会话持久化跟踪。
- **双语界面。** 完整的 i18n 支持,覆盖英文和简体中文,随时可切换。
- **良好的通用扩展能力。** Plugins、MCP Servers 与 Skills 通过 `Extensions Center` 形成统一的扩展入口与产品模型。
- **ACP Server 支持。** TiyCode 可作为无头 ACP(Agent Client Protocol)服务器运行,通过 `tiycode acp --stdio` 或 `tiycode acp --http <addr>` 启动,让外部工具和 IDE 插件通过标准 JSON-RPC 协议驱动 Agent 运行时,无需启动桌面 GUI。
- **IM 通道网关。** 将 TiyCode 接入微信或企业微信,扫码登录后即可在聊天应用中直接与 Agent 对话——发送消息和附件、接收流式回复,无需打开桌面 GUI。
- **良好的通用扩展能力。** Plugins、MCP Servers 与 Skills 通过 `Extensions Center` 形成统一的扩展入口与产品模型。
- **更友好的日常体验。** 支持结构化参数解析的 Slash Command、智能会话标题、上下文压缩、Commit Message 生成、包含 Ghostty 在内的外部终端衔接以及紧凑工作台控件,让协作过程更顺手、更连贯。
- **线程级别耗时计时器。** 跟踪每个线程的活跃执行时间,排除暂停时间,并支持跨会话持久化跟踪。
- **内置 Runtime。** 主执行链路 `Frontend -> Rust Core -> BuiltInAgentRuntime -> tiycore -> LLM`。
- **双语界面。** 完整的 i18n 支持,覆盖英文和简体中文,随时可切换。

## 技术栈

Expand Down
17 changes: 17 additions & 0 deletions src-tauri/migrations/20260607000000_goal_judge_fields.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- Goal Judge verification fields: persist the most recent independent Judge
-- verdict for a goal. Acceptance is expressed as status='complete' AND
-- judge_passed=1 (the main agent can no longer self-attest completion).
ALTER TABLE goals ADD COLUMN judge_passed INTEGER NOT NULL DEFAULT 0; -- bool
ALTER TABLE goals ADD COLUMN judge_completeness INTEGER; -- 0-100, nullable
ALTER TABLE goals ADD COLUMN judge_findings TEXT; -- JSON array, nullable
ALTER TABLE goals ADD COLUMN judge_summary TEXT; -- nullable
ALTER TABLE goals ADD COLUMN judge_evaluated_run_id TEXT; -- nullable

-- Backfill goals already completed via the legacy goal_scored path so that an
-- upgrade does not treat them as un-verified (which would otherwise let goal
-- continuation re-open them).
UPDATE goals
SET judge_passed = 1,
judge_summary = COALESCE(judge_summary, evidence),
judge_completeness = COALESCE(judge_completeness, 100)
WHERE status = 'complete';
5 changes: 5 additions & 0 deletions src-tauri/migrations/20260607000001_drop_goal_time_used.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- Drop goal-level time accounting. Time-tracking moved to thread_runs.elapsed_running_secs
-- (added by 20260604000000_run_elapsed_tracking.sql), which is summed across all of a thread's
-- runs (planning + implementation) and rendered by the workbench-shell timer. The goal-level
-- time_used_seconds column was write-only with no readers in budget enforcement, UI, or logging.
ALTER TABLE goals DROP COLUMN time_used_seconds;
41 changes: 0 additions & 41 deletions src-tauri/src/commands/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -624,47 +624,6 @@ pub async fn goal_pause(
match goal {
Some(g) => {
if g.status == crate::model::goal::GoalStatus::Active {
// Account elapsed time of any currently active run before pausing
if let Some(run_seconds) =
crate::persistence::repo::run_repo::get_active_run_elapsed_seconds(
&state.pool,
&thread_id,
)
.await
.unwrap_or(None)
{
let active_run_id = crate::persistence::repo::run_repo::find_latest_by_thread(
&state.pool,
&thread_id,
)
.await
.ok()
.flatten()
.and_then(|run| {
matches!(
run.status.as_str(),
"running" | "waiting_approval" | "needs_reply"
)
.then_some(run.id)
});
let paused_seconds = active_run_id
.as_deref()
.map(|run_id| {
let mut guard =
state.goal_runtime_state.lock().unwrap_or_else(|poisoned| {
tracing::warn!(
"goal_pause: goal runtime mutex poisoned, recovering"
);
poisoned.into_inner()
});
guard.take_run_paused_seconds(run_id).max(0)
})
.unwrap_or(0);
let billable_seconds = (run_seconds - paused_seconds).max(0);
if billable_seconds > 0 {
mgr.account_usage(&g.id, 0, billable_seconds).await.ok();
}
}
mgr.pause(&g.id, crate::model::goal::PauseReason::UserRequested, None)
.await?;
}
Expand Down
34 changes: 0 additions & 34 deletions src-tauri/src/core/agent_run_event_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,33 +184,6 @@ pub(crate) fn sidebar_status_for_runtime_event(
}

impl AgentRunManager {
fn start_goal_run_pause(&self, thread_id: &str, run_id: &str) {
if thread_id.is_empty() {
return;
}
let mut guard = self.goal_runtime_state.lock().unwrap_or_else(|poisoned| {
tracing::warn!("goal pause runtime mutex poisoned, recovering");
poisoned.into_inner()
});
guard.start_run_pause(thread_id, run_id);
}

fn finish_goal_run_pause(&self, run_id: &str) {
let mut guard = self.goal_runtime_state.lock().unwrap_or_else(|poisoned| {
tracing::warn!("goal pause runtime mutex poisoned, recovering");
poisoned.into_inner()
});
guard.finish_run_pause(run_id);
}

fn cleanup_goal_run_pause(&self, run_id: &str) {
let mut guard = self.goal_runtime_state.lock().unwrap_or_else(|poisoned| {
tracing::warn!("goal pause runtime mutex poisoned, recovering");
poisoned.into_inner()
});
guard.cleanup_run_pause(run_id);
}

pub(crate) async fn handle_runtime_channel_closed(
self: &Arc<Self>,
run_id: &str,
Expand Down Expand Up @@ -410,26 +383,22 @@ impl AgentRunManager {
}
ThreadStreamEvent::ApprovalRequired { .. } => {
let thread_id = self.get_thread_id(run_id).await;
self.start_goal_run_pause(&thread_id, run_id);
run_repo::update_status(&self.pool, run_id, RunStatus::WaitingApproval).await?;
thread_repo::update_status(&self.pool, &thread_id, &ThreadStatus::WaitingApproval)
.await?;
}
ThreadStreamEvent::ClarifyRequired { .. } => {
let thread_id = self.get_thread_id(run_id).await;
self.start_goal_run_pause(&thread_id, run_id);
run_repo::update_status(&self.pool, run_id, RunStatus::NeedsReply).await?;
thread_repo::update_status(&self.pool, &thread_id, &ThreadStatus::NeedsReply)
.await?;
}
ThreadStreamEvent::ApprovalResolved { .. } => {
self.finish_goal_run_pause(run_id);
run_repo::update_status(&self.pool, run_id, RunStatus::Running).await?;
let thread_id = self.get_thread_id(run_id).await;
thread_repo::update_status(&self.pool, &thread_id, &ThreadStatus::Running).await?;
}
ThreadStreamEvent::ClarifyResolved { .. } => {
self.finish_goal_run_pause(run_id);
run_repo::update_status(&self.pool, run_id, RunStatus::Running).await?;
let thread_id = self.get_thread_id(run_id).await;
thread_repo::update_status(&self.pool, &thread_id, &ThreadStatus::Running).await?;
Expand Down Expand Up @@ -458,7 +427,6 @@ impl AgentRunManager {
}
ThreadStreamEvent::RunCheckpointed { .. } => {
let thread_id = self.get_thread_id(run_id).await;
self.start_goal_run_pause(&thread_id, run_id);
run_repo::update_status(&self.pool, run_id, RunStatus::WaitingApproval).await?;
thread_repo::update_status(&self.pool, &thread_id, &ThreadStatus::WaitingApproval)
.await?;
Expand All @@ -476,7 +444,6 @@ impl AgentRunManager {
| ThreadStreamEvent::RunFailed { error, .. } => Some(error.as_str()),
_ => None,
};
self.finish_goal_run_pause(run_id);
self.finish_run(run_id, final_status, error_message).await?;
let thread_id = self.get_thread_id(run_id).await;
if let Some(frontend_tx) = self.frontend_tx_for_run(run_id).await {
Expand Down Expand Up @@ -567,7 +534,6 @@ impl AgentRunManager {
);
}
}
self.cleanup_goal_run_pause(run_id);
}

Ok(())
Expand Down
Loading
Loading