Commit graph

561 commits

Author SHA1 Message Date
Darkatse
c369a64d5f feat(agent): add concurrent background worker scheduling for SubAgents 2026-05-29 00:57:19 +08:00
Darkatse
4d9e210bb6 feat(agent): add first-stage return-mode SubAgent runtime 2026-05-28 02:30:29 +08:00
Darkatse
0fdfda1fc4 feat(agent): add independent preset/model prompt assembly pipeline 2026-05-27 20:30:36 +08:00
Darkatse
8aef6a8272 feat(agent): add backend LLM connection model bindings 2026-05-26 21:26:28 +08:00
Darkatse
27d2e05943 fix(connection-manager): defer validation during profile replay 2026-05-26 12:44:49 +08:00
Darkatse
ea0f3c2a72 fix(storage): harden mobile file moves and chat search contracts #74 2026-05-26 00:10:34 +08:00
Darkatse
41754be9a2 fix(generation): unblock UI after pre-request Generate failures 2026-05-25 00:35:00 +08:00
Darkatse
3f328f0950 fix(data-migration): disambiguate SillyTavern user backup imports 2026-05-23 01:12:36 +08:00
Darkatse
9e293ae764 fix(groups): tolerate legacy group manifest field types 2026-05-23 00:16:52 +08:00
Darkatse
a106a5e486 fix(agent): hide Skill index metadata from skill_list results 2026-05-22 23:49:57 +08:00
Darkatse
ab726462d0 fix(agent): use text metrics and typed tool result payloads 2026-05-22 23:15:54 +08:00
Darkatse
6562dd7a8c fix (openrouter): Fix the OpenRouter App Attribution request header. 2026-05-22 18:20:08 +08:00
Darkatse
9614b6942a chore(agent): rename Skill Manager extension label to Skill 2026-05-22 01:31:04 +08:00
Darkatse
700ad5843d fix(build): isolate webpack caches and keep Agent host imports runtime-only 2026-05-22 00:59:28 +08:00
Darkatse
f0d6eccae7 fix(agent): preserve tool-call chains through prompt post-processing 2026-05-21 23:37:07 +08:00
Darkatse
4ba830a507 fix(agent): freeze run input history for swipe and persist state 2026-05-21 23:06:19 +08:00
Darkatse
1c4c86c99a feat(agent): sync scoped Skills with portable presets and cards 2026-05-21 22:22:36 +08:00
Darkatse
766184b8ac feat(agent): add scoped Agent Skill management with rename retargeting 2026-05-21 12:38:18 +08:00
Darkatse
2750b58370 fix(agent): capture direct outputs and update default prompt 2026-05-21 00:17:59 +08:00
Darkatse
84eaadc337 fix(agent): materialize Agent system prompts through PromptManager 2026-05-20 23:38:33 +08:00
Darkatse
77f15e2196 fix(agent): publish persistent state IDs only after durable finish 2026-05-20 21:27:05 +08:00
Darkatse
c26e430ace fix(agent): make Agent Results a reserved no-op prompt marker 2026-05-20 20:17:21 +08:00
Darkatse
d6aa9733ba Merge branch 'sync/upstream-1.18.0' into next/2.0.0 2026-05-20 19:13:09 +08:00
Darkatse
37d9a2649c feat(openai): add shared custom model management for chat providers #40 #69 2026-05-20 18:19:28 +08:00
Darkatse
ea3f5122ce fix(agent): fail fast /trigger Agent routing errors 2026-05-20 15:02:26 +08:00
C.J. Wu
2e03515b97
Merge pull request #58 from calm-prussian/next/v2.0.0/trigger_agent_clean
feat: 允许 /trigger 斜杠命令触发 Agent 模式
2026-05-20 14:24:56 +08:00
Darkatse
07715203c3 fix(agent): type workspace directory errors 2026-05-20 14:11:54 +08:00
Darkatse
567be35106 fix(agent): emit MESSAGE_UPDATED after rollback #70 2026-05-20 13:15:29 +08:00
Darkatse
7e8c003b31 fix(agent): harden upstream invalid response retries 2026-05-20 12:57:37 +08:00
Darkatse
fe9a479299 fix(agent): enforce typed retry and fail-fast rollback contracts 2026-05-20 12:27:05 +08:00
Darkatse
b34062aa70 fix(agent): preserve committed output as partial success 2026-05-20 00:16:10 +08:00
Darkatse
b68e962f24 fix(agent): align drift recovery and rollback semantics 2026-05-19 23:00:07 +08:00
C.J. Wu
143c217226
Merge pull request #68 from yhny1001/feat/agent-drift-recovery
feat(agent): soft drift recovery for model.tool_call_required + strengthen workspace.commit hint
2026-05-19 22:09:43 +08:00
C.J. Wu
d8e453b9c7
Merge branch 'next/2.0.0' into feat/agent-drift-recovery 2026-05-19 22:05:38 +08:00
C.J. Wu
2809e7fcf7
Merge pull request #63 from yhny1001/fix/issue-55-frontend-ui
fix(agent): wire #55 frontend UI to userRetryable + run_rollback_targets
2026-05-19 19:22:08 +08:00
yhny1001
73a22d743a feat(agent): soft drift recovery + strengthen workspace.commit hint (#64)
Background
----------
Issue surfaced in production: model uses tools correctly through the
full pipeline (write_file → commit), then on the round expected to call
workspace_finish, replies with the full content as plain assistant text
instead. The committed message survives but the run fails with
`model.tool_call_required`, leaving an orphaned chat message and the UI
locked. PR #62 (issue #55) added rollback signalling so the host can
unwind cleanly, but the model's work is still discarded.

Two complementary fixes:

1. Strengthen `workspace.commit`'s tool result string with an explicit
   "REQUIRED NEXT STEP: call workspace_finish" reminder + a
   `nextRequiredTool` field in structured payload. This is the highest
   attention real-estate in the prompt (last tool result the model sees
   before the next turn) and costs essentially nothing.

2. Soft drift recovery: when the model returns 0 tool calls, give it
   ONE corrective nudge instead of failing immediately. We push the
   model's drifted text into history (so it owns what it said) and
   append a synthetic `user` reminder that distinguishes the post-commit
   case (commit will be rolled back) from the no-commit case (every
   turn must use a tool). If the model recovers (any tool call) the
   run continues normally; if it drifts again, we fall through to the
   existing #55 path (rollback + `userRetryable=true`).

   Hard cap of 1 attempt per run via `DRIFT_RECOVERY_MAX_ATTEMPTS`
   prevents token-bombing on stubborn drifters. Multi-turn pattern
   verified against Anthropic + OpenAI docs as API-compatible (no
   role-alternation constraint after no-tool-use turn).

Backward compatibility
----------------------
- Failed recovery falls through to existing #55 code path: same
  `model.tool_call_required` error, same `run_rollback_targets` event,
  same `userRetryable=true`. No frontend changes required for the
  failure case.
- New `drift_recovery_attempted` event is additive; old hosts ignore.
- Existing #55 drift test extended to include the recovery attempt
  before falling through to rollback.

Tests
-----
- `foreground_run_emits_rollback_targets_on_tool_call_required_drift`:
  updated to assert exactly one `drift_recovery_attempted` precedes the
  rollback when the model drifts twice in a row.
- `foreground_run_recovers_from_post_commit_drift_with_nudge`: new —
  model drifts after commit, gets nudged, complies with finish; run
  completes, no rollback, commit preserved. Also asserts the nudge text
  reaches the model verbatim.
- `foreground_run_recovers_from_no_commit_drift_with_nudge`: new —
  model drifts on turn 1, gets nudged, completes the full pipeline.

601 lib tests passing.

Docs
----
- `RunEventJournal.md`: documents `drift_recovery_attempted` event +
  recovery flow.
- `ToolSystem.md`: spells out the soft-recovery contract and which
  drift codes do/don't get the second chance.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 19:08:20 +08:00
yhny1001
1d85e737be fix(agent): preserve prior swipes when rolling back drift commits
PR review surfaced that the rollback path for `run_rollback_targets`
was always calling `deleteMessage(index)`, which wipes the entire chat
entry — including any swipes the user had authored before the agent
run started. For regenerate / swipe generation types the run only
appends a new swipe in-place via `saveReply({ type: 'swipe' })`, so
nuking the whole message is too aggressive.

Captures the rollback strategy at commit time on
`extra.tauritavern.agent.rollback`:

- If the first `saveReply` call grew `chat.length` (the run created a
  new chat entry), strategy is `deleteMessage`.
- Otherwise the run appended a swipe to a pre-existing assistant
  message, strategy is `deleteSwipe` with the new swipe's id.

The rollback helper now branches on that strategy: it calls vendor's
exported `deleteSwipe(swipeId, messageId)` for swipe-mode targets
(only when the swipes array still has more than one entry so vendor's
"can't delete the last swipe" toastr never fires), and falls back to
`deleteMessage` otherwise. Messages persisted before this field
existed default to the old `deleteMessage` behavior, so no migration
is needed.

No vendor file is touched: `deleteSwipe` is already exported from
`src/script.js`, the bridge change lives in
`src/tauri/main/api/agent-chat-commit-bridge.js`, and the rollback
helper is project-owned.

Tests: two new cases on the rollback helper — one drives the
deleteSwipe path against a 3-swipe message and asserts that the
prior swipes (`'user-authored'`, `'user-authored alt 1'`) survive
while the agent's appended swipe is popped; the other asserts the
fallback to `deleteMessage` when the swipe array is a singleton.

Also rebased onto current next/2.0.0 so the duplicate
cherry-picked backend commit drops cleanly and the PR diff stays
frontend-only as promised in the PR description.

Refs PR review feedback on Darkatse/TauriTavern#63.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 18:51:28 +08:00
yhny1001
ca1f4d3244 fix(agent): wire #55 frontend UI to userRetryable + run_rollback_targets
The backend started emitting run_rollback_targets and a userRetryable
flag on run_failed when an Agent run drifts off-tool (#55), but the
host shell was ignoring both. As a result drift would leave orphan chat
messages and the user had no in-UI Retry affordance. This wires three
host-side loops without touching vendor:

1. Retry entry. `presentAgentRunFailure` now also returns
   `userRetryable`, and the run-failure detail section adds an explicit
   Retry button (calling `$('#option_regenerate').trigger('click')`, the
   existing regenerate path) plus a User-retryable field. The timeline
   panel auto-expands and opens the failure details when a user-
   retryable failure arrives so the button is immediately visible.
2. Drift rollback. `agent-run-controller` now subscribes to
   `run_rollback_targets` and delegates to the new
   `agent-run-message-rollback` helper, which uses vendor-exported
   `deleteMessage` against the host's own messageId (the chat array
   index returned via `resolve_agent_chat_commit`). Matches are gated
   on `extra.tauritavern.agent.runId === runId` so a re-saved message
   from another run is never wiped, missing messages are silently
   skipped, and ids are deleted back-to-front so splice indices stay
   valid.
3. Unlocking send. The controller now defers resolving / rejecting the
   run promise until the rollback Promise settles. Vendor's existing
   `finally { unblockGeneration(type); }` in Generate() therefore fires
   after the chat is clean, so the send button (and other
   `is_send_press` gates) are released against the post-rollback state
   rather than mid-rollback. No vendor file is touched.

Friendly drift-error copy is also added for `agent.tool_after_finish`
and `agent.max_tool_rounds_exceeded`, and the new i18n keys
(`timelineActionRetry`, `timelineDetailFieldUserRetryable`,
`timelineActionRetryHint`) are mirrored in en, zh-cn and zh-tw to keep
the i18n contract test green.

Test plan: pnpm run check:frontend, pnpm run check:types,
pnpm run test:contracts (246 tests, all green; 4 new tests cover
presenter, formatter, controller-await-rollback, and the rollback
helper itself).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 18:47:28 +08:00
C.J. Wu
938742a225
Merge pull request #62 from yhny1001/fix/issues-54-55
fix(agent): translate directory access to recoverable error (#54) + emit run_rollback_targets on instruction drift (#55) [backend]
2026-05-19 14:51:14 +08:00
C.J. Wu
d191f732f5
Merge pull request #61 from yhny1001/chore/upstream-body-diagnostics
feat(diagnostics): log upstream body preview when chat completion JSON parse fails + reclassify as transient
2026-05-19 14:45:13 +08:00
Darkatse
399a4f31ee feat(openai): make additional parameters provider-scoped 2026-05-19 00:44:32 +08:00
C.J. Wu
a1344feb7c
Merge pull request #64 from exynos967/feat/2.0-chat-additional-parameters
feat(openai): 允许非自定义提供商添加附加参数
2026-05-19 00:01:59 +08:00
Darkatse
8b9b212b76 sync(upstream): align 1.18 data-root content and assets library 2026-05-18 23:46:48 +08:00
Darkatse
78e0f0ca13 sync(upstream): align 1.18 frontend contracts 2026-05-18 21:57:39 +08:00
yhny1001
99113979a1 fix(agent): reclassify upstream invalid response as transient retryable
Chat completion JSON-parse / body-read failures used to surface as
`DomainError::InternalError`, which `error_payload::is_retryable` treats as
non-retryable `agent.internal_error`. In practice these failures are caused by
upstream-side hiccups (CDN HTML challenges, proxy timeouts mid-body, plain-text
200 errors, SSE on a non-stream endpoint) and tearing down the run is the wrong
default — the user just sees an opaque permanent failure.

Reclassify the helper to return `DomainError::Transient` carrying the
`model.upstream_invalid_response` structured prefix so:

* `From<DomainError> for ApplicationError` maps it to `ApplicationError::Transient`,
* `error_payload::structured_code_and_message` extracts the
  `model.upstream_invalid_response` code so the frontend can present a coherent
  error,
* `error_payload::is_retryable` reports `retryable=true`.

The OpenAI Responses WebSocket message parser uses the same prefix and helper
so WS-frame JSON failures behave identically to HTTP-body JSON failures.

No new `DomainError` variant: this reuses the existing `Transient` variant, so
the three exhaustive `From<DomainError>` / `map_domain_error` match arms are
untouched.

Adds a regression test in `error_payload::tests` asserting that an
`ApplicationError::Transient` whose message starts with
`model.upstream_invalid_response:` produces `code=model.upstream_invalid_response`
and `retryable=true`.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 20:53:59 +08:00
yhny1001
fdc2cea60d feat(diagnostics): log upstream body preview when chat completion JSON parse fails
When an upstream chat-completion provider returns 2xx with a body the client
cannot decode as JSON (HTML challenge pages, plain-text proxy errors, empty
bodies, SSE on a non-stream endpoint, etc.), the failure used to silently bubble
up as `Failed to parse generation JSON` with no log of what was actually
received, leaving operators without any signal to diagnose CDN/proxy issues.

Introduce a shared body-preview helper in
`infrastructure/apis/http_chat_completion_repository/body_preview.rs`:

* `log_upstream_body_parse_failure(provider, operation, status, content_type,
  body, error)` emits a structured `tracing::error!` event with a UTF-8 lossy
  preview of the first 512 bytes plus the full body length, so logs stay bounded
  even when upstream pours out megabytes of HTML.
* `read_upstream_json_body(provider, operation, response)` is the async wrapper
  every HTTP provider call site uses: it reads the body once, attempts JSON
  decode, calls the logger on failure, and surfaces a more diagnostic error
  string (`Failed to parse {operation} JSON from {provider} (status {n})`).

All twelve `response.json::<Value>().await.map_err(...)` call sites across the
seven provider modules (`openai`, `claude`, `cohere`, `makersuite`,
`gemini_interactions`, `vertexai`, `openai_responses`) now route through the
helper. The OpenAI Responses WebSocket message parser
(`parse_ws_event`) also calls the logger before returning so non-JSON frames
emit the same diagnostic.

Error classification is intentionally unchanged in this commit (still
`DomainError::InternalError`); the follow-up will reclassify it as transient.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 20:50:21 +08:00
yhny1001
fb238538ae fix(agent): emit run_rollback_targets + userRetryable on instruction drift (#55)
When the model deviated from the tool contract after committing chat
artifacts (e.g. answered with plain text instead of calling
`workspace_finish`), the run failed with `model.tool_call_required` /
`agent.tool_after_finish` / `agent.max_tool_rounds_exceeded`, but the
already-committed messages were left orphaned in chat and the UI had no
signal to offer a Retry path.

Loop runner now tracks each `AgentToolEffect::ChatCommitted` during a
run. When the run later enters a drift state, it emits a
`run_rollback_targets` warn event BEFORE `run_failed`, listing the
`{ path, mode, messageId, round }` of every commit the host UI needs to
roll back.

`run_failed` payload gains a `userRetryable: bool` flag:
- `retryable=true` (transient / rate-limited) implies `userRetryable=true`.
- Otherwise drift codes (`model.tool_call_required`,
  `agent.tool_after_finish`, `agent.max_tool_rounds_exceeded`) are
  `userRetryable=true` while remaining auto-non-retryable, so the UI can
  surface a manual Retry button after performing the rollback. Policy /
  validation errors stay `userRetryable=false`.

`workspace_commit` description tightened to spell out the
"commit -> finish in the same or next turn, never plain text" contract.

Docs:
- `docs/Agent/RunEventJournal.md`: new `run_rollback_targets` event,
  expanded `run_failed` payload schema with `userRetryable` semantics.
- `docs/Agent/ToolSystem.md`: documents the
  `workspace.path_is_directory` business error and the commit/finish
  drift contract so the tool-system docs stay in sync.

Tests: integration test
`foreground_run_emits_rollback_targets_on_tool_call_required_drift`
drives a mock gateway to commit then drift, asserting that the
rollback event lists the committed message and that the run fails with
`userRetryable=true`. Extended `error_payload` unit tests cover all
three drift codes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 20:03:59 +08:00
yhny1001
44997a1213 fix(agent): translate workspace directory access to recoverable tool error (#54)
`workspace_read_file` and the other workspace IO tools used to crash the
run with a non-retryable `agent.internal_error` when the model targeted
a directory (e.g. `persist`, `output`) instead of a regular file: the
underlying `fs::read_to_string` raised `EISDIR`, which the repository
surfaced as `DomainError::InternalError`.

We now model "the target is a directory" as a domain-level conflict and
let the tool layer translate it into a model-facing recoverable error:

- `DomainError::Conflict(String)` added at the domain layer; mapped to
  `ApplicationError::ValidationError`, `CommandError::BadRequest`, and
  HTTP 409 in the LAN sync server (all 3 exhaustive matches updated).
- `file_agent_repository::workspace_store::ensure_target_is_not_directory`
  probes `fs::symlink_metadata` in `read_text` / `write_text` and raises
  `DomainError::Conflict("workspace.path_is_directory: ...")` before
  hitting `EISDIR`.
- A shared `parse_workspace_conflict_message` lives in
  `agent_tools/common.rs` (re-exported `pub(crate)`) so both the
  workspace tool layer (`classify_workspace_io_error` in `args.rs`) and
  the runtime-service commit hook (`perform_host_chat_commit`) decode
  the conflict into a `workspace.path_is_directory` recoverable tool
  result through a single source of truth.
- `workspace_read_file` description tightened to explicitly forbid
  directory targets and point at `workspace_list_files` instead.

Tests:
- Repository-level: `read_text` / `write_text` on a directory return
  `DomainError::Conflict` with the documented prefix.
- Tool layer: `classify_workspace_io_error` maps Conflict ->
  `workspace.path_is_directory`, NotFound -> `workspace.file_not_found`,
  and bubbles unknown errors for the dispatcher to fail-fast on.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 20:03:24 +08:00
yhny1001
fdeca94534 chore: ignore local .cursor/ skill workspace
`.cursor/` holds personal IDE / Cursor agent skill files (e.g. the
TauriTavern contribution conventions skill) that are local to each
developer and should not flow through code review.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 20:02:50 +08:00
xiaoxi68
2b206be3c5 feat(openai): extend additional parameters to native providers 2026-05-18 16:16:34 +08:00