refactor(account): isolate provider authentication and preserve model selections
This commit is contained in:
1 parent
2fc9262f3e
commit
40da69ff75
302 files changed
+1771
-952
No files matched your search
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-17-account-provider-signout.md
|
||||
2026-09-17-account-provider-signout.md: dd9930a7b36fd93c18febb8cca506e59bb605797
|
||||
2026-09-17-account-provider-signout.zh.md: e706a1bacc3c3ef2c3b29912fa85da59b02d8d5e
|
||||
2026-09-17-account-provider-signout.md: 6b8ef842c54ef25d1e5fc063c40361775aae9a92
|
||||
2026-09-17-account-provider-signout.zh.md: 588dd38bf9fbae8131a9e3c5cb1181dcc807df8c
|
||||
@@ -10,18 +10,18 @@ Selecting credentials from login state changes the billing identity of an existi
|
||||
|
||||
## Decision
|
||||
|
||||
`deepseek-official` and `deepseek-account` are independent routes sharing the DeepSeek protocol implementation and validated connection settings. Each adapter has exactly one credential resolver and authentication header mode. Neither route falls back to the other credential. The account service retains the origin and issuer validation defined by the [login decision](2026-09-14-deepseek-account-login.md).
|
||||
`deepseek-official` and `deepseek-account` are independent routes sharing the DeepSeek protocol implementation. Each adapter has exactly one credential resolver and authentication header mode. Neither route falls back to the other credential. The account service retains the origin and issuer validation defined by the [login decision](2026-09-14-deepseek-account-login.md).
|
||||
|
||||
The Agent running phase owns `activeProvider`, the final provider returned by request preparation. The value remains through tools and retry preparation, changes when the next request binds, and clears before `turn/end`. It is undefined during a turn’s first preparation. A missing account credential still rejects the first request before transport. Token resolution checks local sign-out in progress; no login-level signal, credential lease, or synchronization protocol covers the short token-read/send interval.
|
||||
Account cancellation reads each running Agent’s latest `Session.requestContext()` instead of maintaining another provider field. This logged value remains through tools and retries and follows the next bound request. Idle Agents are excluded. During a new turn’s first preparation it can still name the previous turn’s account route, so sign-out can also interrupt an API-key turn that has not bound its new request yet. A missing account credential rejects a first request without a previous context before transport.
|
||||
|
||||
Successful local sign-out publishes an account event. The DeepSeek plugin enumerates the live Agent registry and cancels matching account routes through `Agent.cancel`, preserving inboxes. Each registered child is classified independently; existing parent-owned cancellation still applies to children whose lifetime is attached to a cancelled parent. Independent API-key agents continue. The existing HTTP signal carries cancellation to the transport.
|
||||
Successful local sign-out publishes an account event. The platform account provider installs the account module’s listener, which enumerates the Agent registry and cancels matching account routes through `Agent.cancel`, preserving inboxes. The account controller shares its predicate for sign-out confirmation. Each registered child is classified independently; existing parent-owned cancellation still applies to children whose lifetime is attached to a cancelled parent. The existing HTTP signal carries cancellation to transport.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
A historical set of providers wrongly cancels a turn after it switches to an API-key route. Current settings misclassify an older in-flight request. A module-global map duplicates Agent lifecycle ownership, while making the account service manage every provider reverses the service dependency. A login-level cancellation signal duplicates the existing Agent and HTTP cancellation chain.
|
||||
A historical set of providers wrongly cancels a turn after it switches to an API-key route. Current settings misclassify an older in-flight request. A second provider field or module-global map duplicates information already recorded in request context. A login-level cancellation signal duplicates the existing Agent and HTTP cancellation chain.
|
||||
|
||||
## Consequences
|
||||
|
||||
Model selection exposes both routes, and choosing an account route requires login even when an API key exists. The account route shares its model catalog and connection settings with the API-key route. Cancellation preserves queued input without waking it automatically. The recorded hook reason produces a localized conversation notice without changing durable event types.
|
||||
Model selection exposes both routes, and choosing an account route requires login even when an API key exists. The `llm-deepseek-account` and `llm-deepseek-api-key` plugins independently own catalogs, connection settings, and credentials; both use the `llm-deepseek` protocol library. Cancellation preserves queued input without waking it automatically. The recorded hook reason produces a localized conversation notice without changing durable event types.
|
||||
|
||||
Behavior tests cover credential separation, first preparation, tool-stage cancellation, and route replacement. The SDK account-provider-signout scenario boots the shipped profile and records interrupted output plus the cancellation cause.
|
||||
@@ -10,18 +10,18 @@ Status: implemented
|
||||
|
||||
## Decision
|
||||
|
||||
`deepseek-official` 和 `deepseek-account` 是独立路由,共享 DeepSeek 协议实现及已校验的连接设置。每个适配器仅有一个凭证解析器和认证请求头模式。两条路由均不回退到另一凭证。账号服务保留[登录决策](2026-09-14-deepseek-account-login.zh.md)定义的来源及签发方校验。
|
||||
`deepseek-official` 和 `deepseek-account` 是独立路由,共享 DeepSeek 协议实现。每个适配器仅有一个凭证解析器和认证请求头模式。两条路由均不回退到另一凭证。账号服务保留[登录决策](2026-09-14-deepseek-account-login.zh.md)定义的来源及签发方校验。
|
||||
|
||||
Agent 运行阶段拥有 `activeProvider`,其值是请求准备返回的最终提供方。该值在工具执行及重试准备期间保留,在绑定下一请求时改变,在 `turn/end` 前清除。轮次的首请求准备期间该值为 undefined。账号凭证缺失仍在首请求进入传输前拒绝请求。Token 解析检查本地退登是否进行中;不为短暂的读取 token 到发送间隙引入登录级信号、凭证租约或同步协议。
|
||||
账号取消读取运行中 Agent 最近的 `Session.requestContext()`,不维护额外提供方字段。该已记录值在工具和重试阶段保留,随下一次绑定请求更新;空闲 Agent 不计入。新轮次首次准备期间,它仍可能指向上一轮的账号路由,因此退登也可能打断尚未绑定新请求的 API key 轮次。没有历史上下文的首请求若缺少账号凭据,会在传输前失败。
|
||||
|
||||
本地退登成功后发布账号事件。DeepSeek 插件遍历运行中的 Agent 注册表,通过 `Agent.cancel` 取消匹配的账号路由并保留收件箱。每个已注册子代理独立分类;生命周期绑定到已取消父代理的子代理仍受现有父级取消规则约束。独立的 API Key 代理继续运行。现有 HTTP 信号将取消传递到传输层。
|
||||
本地退登成功后发布账号事件。Platform 账号提供方安装账号模块的监听器,遍历 Agent 注册表,通过 `Agent.cancel` 取消匹配的账号路由并保留收件箱。账号控制器的退登确认复用同一判断函数。每个已注册子代理独立分类;生命周期绑定到已取消父代理的子代理仍受现有父级取消规则约束。现有 HTTP 信号将取消传递到传输层。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
历史提供方集合会在轮次切换到 API Key 路由后仍错误取消它。当前设置会误判更早的在途请求。模块级全局映射重复拥有 Agent 生命周期,让账号服务管理所有提供方则反转服务依赖。登录级取消信号重复现有 Agent 与 HTTP 取消链路。
|
||||
历史提供方集合会在轮次切换到 API Key 路由后仍错误取消它。当前设置会误判更早的在途请求。额外的提供方字段或模块级全局映射重复保存请求上下文中已有的信息。登录级取消信号重复现有 Agent 与 HTTP 取消链路。
|
||||
|
||||
## Consequences
|
||||
|
||||
模型选择展示两条路由,即使存在 API Key,选择账号路由仍要求登录。账号路由与 API Key 路由共享模型目录和连接设置。取消保留排队输入,不自动唤醒。已记录的 hook 原因生成本地化对话提示,无须改变持久事件类型。
|
||||
模型选择展示两条路由,即使存在 API Key,选择账号路由仍要求登录。两个路由分别由 `llm-deepseek-account` 和 `llm-deepseek-api-key` 注册,拥有独立的模型目录与连接设置,共用 `llm-deepseek` 协议实现。取消保留排队输入,不自动唤醒。已记录的 hook 原因生成本地化对话提示,无须改变持久事件类型。
|
||||
|
||||
行为测试覆盖凭证分离、首请求准备、工具阶段取消及路由替换。SDK account-provider-signout 场景启动随附 profile,记录被中断的输出和取消原因。
|
||||
@@ -190,8 +190,10 @@ flowchart LR
|
||||
cfg --> plugin_dsh_base_agent_loop
|
||||
plugin_dsh_base_fs_sandbox["fs-sandbox<br/>@deepseek-ai/dsh-fs-sandbox"]
|
||||
cfg --> plugin_dsh_base_fs_sandbox
|
||||
plugin_dsh_base_llm_deepseek["llm-deepseek<br/>@deepseek-ai/dsh-llm-deepseek"]
|
||||
plugin_dsh_base_llm_deepseek["llm-deepseek<br/>@deepseek-ai/dsh-llm-deepseek-api-key"]
|
||||
cfg --> plugin_dsh_base_llm_deepseek
|
||||
plugin_dsh_base_llm_deepseek_account["llm-deepseek-account<br/>@deepseek-ai/dsh-llm-deepseek-account"]
|
||||
cfg --> plugin_dsh_base_llm_deepseek_account
|
||||
```
|
||||
|
||||
| Plugin id | Package / module |
|
||||
@@ -287,7 +289,8 @@ flowchart LR
|
||||
| `system-prompt` | `@deepseek-ai/dsh-system-prompt` |
|
||||
| `agent-loop` | `@deepseek-ai/dsh-agent-loop` |
|
||||
| `fs-sandbox` | `@deepseek-ai/dsh-fs-sandbox` |
|
||||
| `llm-deepseek` | `@deepseek-ai/dsh-llm-deepseek` |
|
||||
| `llm-deepseek` | `@deepseek-ai/dsh-llm-deepseek-api-key` |
|
||||
| `llm-deepseek-account` | `@deepseek-ai/dsh-llm-deepseek-account` |
|
||||
|
||||
Source config: [`packages/bundle/base/cordis.patch.yml`](../../packages/bundle/base/cordis.patch.yml).
|
||||
|
||||
|
||||
@@ -120,6 +120,8 @@
|
||||
"@deepseek-ai/dsh-host-webserver": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm-deepseek": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm-deepseek-api-key": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm-deepseek-account": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm-mock-server": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm-pi-ai": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm-replay": "workspace:*",
|
||||
|
||||
@@ -1246,7 +1246,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
['deepseek-llm-api-extensions', '@deepseek-ai/dsh-deepseek-llm-api-extensions'],
|
||||
['session-log-deepseek', '@deepseek-ai/dsh-session-log-deepseek'],
|
||||
['plugin-package-inventory-deepseek', '@deepseek-ai/dsh-plugin-package-inventory-deepseek'],
|
||||
['llm-deepseek', '@deepseek-ai/dsh-llm-deepseek'],
|
||||
['llm-deepseek', '@deepseek-ai/dsh-llm-deepseek-api-key'],
|
||||
['sandbox', '@deepseek-ai/dsh-sandbox-local'],
|
||||
['session-projection', '@deepseek-ai/dsh-session-projection'],
|
||||
['sandbox-policy', '@deepseek-ai/dsh-sandbox-policy'],
|
||||
|
||||
@@ -25,7 +25,6 @@ try {
|
||||
options: {},
|
||||
session,
|
||||
inbox: unsupportedInbox(),
|
||||
activeProvider: undefined,
|
||||
status: 'idle',
|
||||
send: () => {},
|
||||
followup: () => {},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Keyless generic ACP v1 control-surface patch over `dsh --profile acp`.
|
||||
|
||||
- id: llm-deepseek
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
name: '@deepseek-ai/dsh-llm-deepseek-api-key'
|
||||
disabled: true
|
||||
|
||||
- id: acp
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# local provider endpoint is supplied by the snapshot test; the real attachment
|
||||
# store and ACP bridge carry two uploaded images into one model request.
|
||||
- id: llm-deepseek
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
name: '@deepseek-ai/dsh-llm-deepseek-api-key'
|
||||
config:
|
||||
apiKeyEnv: DSH_SNAPSHOT_API_KEY
|
||||
baseURL: !!js process.env.DSH_SNAPSHOT_BASE_URL
|
||||
|
||||
@@ -24,6 +24,6 @@
|
||||
|
||||
- insert:
|
||||
- id: llm-deepseek-keyless
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
name: '@deepseek-ai/dsh-llm-deepseek-api-key'
|
||||
config:
|
||||
baseURL: 'http://127.0.0.1:9'
|
||||
@@ -8,7 +8,7 @@ import * as BashEnvPlugin from '@deepseek-ai/dsh-shell-env'
|
||||
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
|
||||
import * as ToolBash from '@deepseek-ai/dsh-tool-bash'
|
||||
import * as ToolTodo from '@deepseek-ai/dsh-tool-todo'
|
||||
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
|
||||
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek-api-key'
|
||||
import TokenMeter from '@deepseek-ai/dsh-token-meter'
|
||||
import ToolResultPruner from '@deepseek-ai/dsh-compaction-tool-result-pruner'
|
||||
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
|
||||
@@ -17,7 +17,7 @@ import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local'
|
||||
import * as BashEnvPlugin from '@deepseek-ai/dsh-shell-env'
|
||||
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
|
||||
import * as ToolBash from '@deepseek-ai/dsh-tool-bash'
|
||||
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
|
||||
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek-api-key'
|
||||
import NodeRuntime from '@deepseek-ai/dsh-ptc-runtime-node'
|
||||
import Sandbox from '@deepseek-ai/dsh-sandbox-local'
|
||||
import SandboxPolicy from '@deepseek-ai/dsh-sandbox-policy'
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/architecture.md
|
||||
architecture.md: 09ad613e0a985e149579b7c655a503a4602f8ef6
|
||||
architecture.zh.md: 9eed57f4cc4f6368d8ce3913baae2cb3116055f9
|
||||
architecture.md: 634214943c14fe049f141350cad6518167f52bb1
|
||||
architecture.zh.md: 6082c5c86a900c03017fbf15f6c4abd41a48a9f9
|
||||
@@ -112,7 +112,7 @@ One inbox feeds the driver; injected context waits for a waking message. AgentLo
|
||||
|
||||
`agent/pre-step` decides the accepted input. Listeners may rewrite or reject claimed messages; a rejected or empty first claim closes a durable turn without a step. An enter decision may set `startsRequestSeries`: the loop logs a fresh `request/header` (reason `series`, or `change` with `startsSeries: true` when the envelope also changed). Wrapping listeners preserve that declaration with `{ ...decision, messages }`. After assembly and `step/start`, `agent/request` and `prepareCall()` resolve the actual route before the system prompt and accepted users are committed; cancellation during either async phase commits neither. The prepared call capability governs prompt admission, not the preceding `request/context`. Every attempt synchronously reconciles the same rendered assembly, appends users only on the first attempt, logs header/context as needed, and derives and freezes the request before streaming the bound call. Retries do not repeat assembly or `agent/pre-step`. Surface replacements and image-offload decisions after attachment start a new request series, including during the first resumed pre-step; unchanged resume continues the series. The first admitted step reserves the system head before user messages even for an empty prompt (no wire message). The prompt travels only as `system/message` history: an empty rendering clears all active system nodes, leaving no old prompt model-visible; capable routes can append non-empty updates after the cached prefix; incapable routes and new request series consolidate non-empty prompt text at the first system node, with logged empty replacements for non-empty later system nodes ([decision](../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md); [decision rule](../packages/core/agent-loop/README.md#understand-the-implementation)).
|
||||
|
||||
The loop sends immutable requests with live cancellation and reuses freeze evidence for frozen identities; [agent-loop](../packages/core/agent-loop/README.md) owns construction and cancellation causes. `Agent.activeProvider` identifies the latest prepared request’s provider.
|
||||
The loop sends immutable requests with live cancellation and reuses freeze evidence for frozen identities; [agent-loop](../packages/core/agent-loop/README.md) owns construction and cancellation causes.
|
||||
|
||||
Details: the [sequence diagram](agent-lifecycle.md), the [tool pipeline](tool-execution-pipeline.md), and [cancellation and error recovery](subsystems/core.md#the-agent-handle).
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ turn/end
|
||||
|
||||
`agent/pre-step` 决定接纳的输入。监听器可以改写或拒绝已领取消息;首次领取被拒绝或为空时,关闭不含步骤的持久轮次。enter 决策可设置 `startsRequestSeries`:循环记录新的 `request/header`(原因为 `series`,或在封装同时变化时为携带 `startsSeries: true` 的 `change`)。包装监听器通过 `{ ...decision, messages }` 保留该声明。组装与 `step/start` 之后,`agent/request` 和 `prepareCall()` 先解析实际路由,再提交系统提示词与已接纳用户消息;在任一异步阶段取消都不会提交这两者。提示词准入依据已准备调用的能力,而非先前的 `request/context`。每次尝试同步协调同一份已渲染组装结果、仅在首次尝试追加用户消息、按需记录 header/context、派生并冻结请求,再通过绑定调用发起流式请求。重试不重复组装或 `agent/pre-step`。附接后的 surface 替换和图片省略决定开启新请求序列,包括恢复后的首次 pre-step 中发生的替换;未变化的恢复延续序列。首次接纳的步骤在用户消息之前预留系统头节点,即使提示词为空(不产生协议消息)。提示词仅通过 `system/message` 历史传递:空渲染文本清除所有生效的系统节点,模型不再看到旧提示词;具备能力的路由可在缓存前缀之后追加非空更新;不具备能力的路由与新请求序列将非空提示词文本归并到首个系统节点,并为非空的后续系统节点记录空内容替换([决策](../.agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.zh.md);[决策规则](../packages/core/agent-loop/README.zh.md#understand-the-implementation))。
|
||||
|
||||
循环发送不可变请求,同时保持取消有效,仅对完全冻结的对象复用冻结证据;[agent-loop](../packages/core/agent-loop/README.zh.md) 负责请求构建与取消原因。`Agent.activeProvider` 标识最近一次已准备请求的提供方。
|
||||
循环发送不可变请求,同时保持取消有效,仅对完全冻结的对象复用冻结证据;[agent-loop](../packages/core/agent-loop/README.zh.md) 负责请求构建与取消原因。
|
||||
|
||||
详情见[时序图](agent-lifecycle.zh.md)、[工具流水线](tool-execution-pipeline.zh.md)和[取消与错误恢复](subsystems/core.zh.md#the-agent-handle)。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/config-catalog.md
|
||||
config-catalog.md: 4df7032411d67f888c395faee700eb3f9729dfed
|
||||
config-catalog.zh.md: bf5931d1726e7a5b8df4742bd925942c950c8511
|
||||
config-catalog.md: 6bc5481042c4c7db272404c97fbb1225fed227df
|
||||
config-catalog.zh.md: bb48754fb2724f9dae49a4d0b12f50caa6525ded
|
||||
+23
-80
@@ -1430,96 +1430,38 @@ export interface Config {
|
||||
|
||||
Source: [`packages/jobs/jobs-local/src/index.ts:45`](../packages/jobs/jobs-local/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-llm-deepseek"></a>
|
||||
<a id="deepseek-aidsh-llm-deepseek-account"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-llm-deepseek`
|
||||
## `@deepseek-ai/dsh-llm-deepseek-account`
|
||||
|
||||
Requires: `llm`
|
||||
|
||||
```ts config-catalog
|
||||
/**
|
||||
* Plugin config, validated by the same-named schemastery schema and doubling
|
||||
* as the `llm-deepseek` settings-section shape. Every field is optional in
|
||||
* yml: a missing API key resolves through {@link Config.apiKeyEnv} at each
|
||||
* request (a request without any key fails with `MISSING_CREDENTIAL`, not at
|
||||
* plugin load), omitted thinking mode uses the provider default, and omitted
|
||||
* reasoning effort resolves to `high`.
|
||||
*/
|
||||
export interface Config {
|
||||
/** Credential reference (environment-variable name) resolved per request; defaults to `DEEPSEEK_API_KEY`. */
|
||||
apiKeyEnv: Volatile<string>
|
||||
/** Endpoint base; falls back to $DEEPSEEK_BASE_URL from a trusted environment layer, then the public API. */
|
||||
baseURL: Volatile<string | undefined>
|
||||
/** Deployment thinking policy; `disabled` limits every conversation request to `off`. */
|
||||
thinking: Volatile<'enabled' | 'disabled' | undefined>
|
||||
/** Default thinking effort (default `high`); `off` disables thinking per request. */
|
||||
reasoningEffort: Volatile<'off' | 'low' | 'high' | 'max' | undefined>
|
||||
/** Default per-request output cap (default 256,000); a model's own cap and explicit request values win. */
|
||||
maxTokens: Volatile<number>
|
||||
/** Positive context capacity used when the selected model has no exact value (default 1,000,000). */
|
||||
defaultContextWindow: Volatile<number>
|
||||
/** Advisory models shown by discovery consumers; defaults to V41 Flash and V4 Pro. */
|
||||
models: Volatile<DeepSeekCatalogModel[]>
|
||||
/** Maximum provider idle time while one stream read is outstanding (default five minutes). */
|
||||
streamIdleTimeoutMs: Volatile<number>
|
||||
/** Maximum accumulated file-referenced image bytes per chat request (default 128 MiB). */
|
||||
maxRequestFilesBytes: Volatile<number>
|
||||
/** Maximum accumulated base64 image payload after Files API fallback (default 20 MiB). */
|
||||
maxInlineRequestImageBytes: Volatile<number>
|
||||
/** Maximum number of represented images per chat request (default 600). */
|
||||
maxImagesPerRequest: Volatile<number>
|
||||
/** Raw-byte removal step after the request exceeds its file bound (default 64 MiB). */
|
||||
imageOffloadByteQuantum: Volatile<number>
|
||||
/** Base64-byte removal step after inline fallback exceeds its bound (default 10 MiB). */
|
||||
inlineImageOffloadByteQuantum: Volatile<number>
|
||||
/** Image-count removal step after the request exceeds its count bound (default 20). */
|
||||
imageOffloadCountQuantum: Volatile<number>
|
||||
/** Maximum duration of one request-image Files API resolution (default one minute). */
|
||||
filesApiTimeoutMs: Volatile<number>
|
||||
/** Explicit lifetime assigned to each uploaded image (default seven days). */
|
||||
fileExpiresAfterSeconds: Volatile<number>
|
||||
/** Remaining lifetime below which an indexed file is replaced (default one hour). */
|
||||
fileRefreshMarginSeconds: Volatile<number>
|
||||
/** Oldest harness-owned files deleted before one quota-recovery upload retry (default 100). */
|
||||
fileQuotaCleanupBatch: Volatile<number>
|
||||
/** Provider-owned model-request retry policy; omission uses normal mode with five retries. */
|
||||
retryPolicy: Volatile<RetryPolicyConfig | undefined>
|
||||
}
|
||||
/** Account route configuration; authentication comes exclusively from the account service. */
|
||||
export type Config = ProtocolConfig
|
||||
```
|
||||
|
||||
/** One optional model entry advertised by the direct-fetch adapter. */
|
||||
export interface DeepSeekCatalogModel {
|
||||
/** Wire model id accepted by the configured endpoint. */
|
||||
id: string
|
||||
/** Selector label; defaults to {@link id}. */
|
||||
name?: string
|
||||
/** Optional selector detail for deployments with similar model variants. */
|
||||
description?: string
|
||||
/** Known combined request/response context capacity; omitted when deployment metadata is unavailable. */
|
||||
contextWindow?: number
|
||||
/** Per-request output cap for this model; omission falls back to the profile's {@link DeepSeekConnectionOptions.maxTokens}. */
|
||||
maxTokens?: number
|
||||
/** Accepted request modalities; omission is text-only. */
|
||||
inputModalities?: ModelModality[]
|
||||
/**
|
||||
* Total-pixel budget replacing the published token-grid projection for one
|
||||
* deterministic request preview, or the 512-by-512 `low` preset; omission
|
||||
* projects onto the token grid.
|
||||
*/
|
||||
imagePixelBudget?: number | 'low'
|
||||
/** Encoded-byte target for one deterministic request preview; the smallest quality-ladder output is used when no quality fits. */
|
||||
imageMaxBytes?: number
|
||||
/**
|
||||
* `'in-history'` declares that the endpoint reads the latest `system`
|
||||
* message at any position of the conversation as the complete effective
|
||||
* system prompt; omission means only a leading system message is read.
|
||||
*/
|
||||
systemPromptUpdate?: SystemPromptUpdate
|
||||
Depends on: [`ProtocolConfig`](../packages/llm/llm-deepseek/src/index.ts)
|
||||
|
||||
Source: [`packages/llm/llm-deepseek-account/src/config.ts:5`](../packages/llm/llm-deepseek-account/src/config.ts)
|
||||
|
||||
<a id="deepseek-aidsh-llm-deepseek-api-key"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-llm-deepseek-api-key`
|
||||
|
||||
Requires: `llm`
|
||||
|
||||
```ts config-catalog
|
||||
/** Messages configuration with a per-request API-key reference. */
|
||||
export interface Config extends ProtocolConfig {
|
||||
/** Credential reference resolved per request; defaults to DEEPSEEK_API_KEY. */
|
||||
apiKeyEnv: Volatile<string>
|
||||
}
|
||||
```
|
||||
|
||||
Depends on: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · [`SystemPromptUpdate`](../packages/llm/llm/src/index.ts) · `Volatile` (`@deepseek-ai/cordis`)
|
||||
Depends on: [`ProtocolConfig`](../packages/llm/llm-deepseek/src/index.ts) · `Volatile` (`@deepseek-ai/cordis`)
|
||||
|
||||
Source: [`packages/llm/llm-deepseek/src/config.ts:28`](../packages/llm/llm-deepseek/src/config.ts)
|
||||
Source: [`packages/llm/llm-deepseek-api-key/src/config.ts:10`](../packages/llm/llm-deepseek-api-key/src/config.ts)
|
||||
|
||||
<a id="deepseek-aidsh-llm-pi-ai"></a>
|
||||
|
||||
@@ -4247,6 +4189,7 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them.
|
||||
- `@deepseek-ai/dsh-http-proxy` ([`packages/util/http-proxy/src/index.ts`](../packages/util/http-proxy/src/index.ts))
|
||||
- `@deepseek-ai/dsh-launch-environment` ([`packages/util/launch-environment/src/index.ts`](../packages/util/launch-environment/src/index.ts))
|
||||
- `@deepseek-ai/dsh-lazy-require` ([`packages/util/lazy-require/src/index.ts`](../packages/util/lazy-require/src/index.ts))
|
||||
- `@deepseek-ai/dsh-llm-deepseek` ([`packages/llm/llm-deepseek/src/index.ts`](../packages/llm/llm-deepseek/src/index.ts))
|
||||
- `@deepseek-ai/dsh-llm-mock-server` ([`packages/test-support/llm-mock-server/src/index.ts`](../packages/test-support/llm-mock-server/src/index.ts))
|
||||
- `@deepseek-ai/dsh-loader-smoke` ([`packages/test-support/loader-smoke/src/index.ts`](../packages/test-support/loader-smoke/src/index.ts))
|
||||
- `@deepseek-ai/dsh-native-command` ([`packages/util/native-command/src/index.ts`](../packages/util/native-command/src/index.ts))
|
||||
|
||||
+23
-80
@@ -1432,96 +1432,38 @@ export interface Config {
|
||||
|
||||
来源: [`packages/jobs/jobs-local/src/index.ts:45`](../packages/jobs/jobs-local/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-llm-deepseek"></a>
|
||||
<a id="deepseek-aidsh-llm-deepseek-account"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-llm-deepseek`
|
||||
## `@deepseek-ai/dsh-llm-deepseek-account`
|
||||
|
||||
需要: `llm`
|
||||
|
||||
```ts config-catalog
|
||||
/**
|
||||
* Plugin config, validated by the same-named schemastery schema and doubling
|
||||
* as the `llm-deepseek` settings-section shape. Every field is optional in
|
||||
* yml: a missing API key resolves through {@link Config.apiKeyEnv} at each
|
||||
* request (a request without any key fails with `MISSING_CREDENTIAL`, not at
|
||||
* plugin load), omitted thinking mode uses the provider default, and omitted
|
||||
* reasoning effort resolves to `high`.
|
||||
*/
|
||||
export interface Config {
|
||||
/** Credential reference (environment-variable name) resolved per request; defaults to `DEEPSEEK_API_KEY`. */
|
||||
apiKeyEnv: Volatile<string>
|
||||
/** Endpoint base; falls back to $DEEPSEEK_BASE_URL from a trusted environment layer, then the public API. */
|
||||
baseURL: Volatile<string | undefined>
|
||||
/** Deployment thinking policy; `disabled` limits every conversation request to `off`. */
|
||||
thinking: Volatile<'enabled' | 'disabled' | undefined>
|
||||
/** Default thinking effort (default `high`); `off` disables thinking per request. */
|
||||
reasoningEffort: Volatile<'off' | 'low' | 'high' | 'max' | undefined>
|
||||
/** Default per-request output cap (default 256,000); a model's own cap and explicit request values win. */
|
||||
maxTokens: Volatile<number>
|
||||
/** Positive context capacity used when the selected model has no exact value (default 1,000,000). */
|
||||
defaultContextWindow: Volatile<number>
|
||||
/** Advisory models shown by discovery consumers; defaults to V41 Flash and V4 Pro. */
|
||||
models: Volatile<DeepSeekCatalogModel[]>
|
||||
/** Maximum provider idle time while one stream read is outstanding (default five minutes). */
|
||||
streamIdleTimeoutMs: Volatile<number>
|
||||
/** Maximum accumulated file-referenced image bytes per chat request (default 128 MiB). */
|
||||
maxRequestFilesBytes: Volatile<number>
|
||||
/** Maximum accumulated base64 image payload after Files API fallback (default 20 MiB). */
|
||||
maxInlineRequestImageBytes: Volatile<number>
|
||||
/** Maximum number of represented images per chat request (default 600). */
|
||||
maxImagesPerRequest: Volatile<number>
|
||||
/** Raw-byte removal step after the request exceeds its file bound (default 64 MiB). */
|
||||
imageOffloadByteQuantum: Volatile<number>
|
||||
/** Base64-byte removal step after inline fallback exceeds its bound (default 10 MiB). */
|
||||
inlineImageOffloadByteQuantum: Volatile<number>
|
||||
/** Image-count removal step after the request exceeds its count bound (default 20). */
|
||||
imageOffloadCountQuantum: Volatile<number>
|
||||
/** Maximum duration of one request-image Files API resolution (default one minute). */
|
||||
filesApiTimeoutMs: Volatile<number>
|
||||
/** Explicit lifetime assigned to each uploaded image (default seven days). */
|
||||
fileExpiresAfterSeconds: Volatile<number>
|
||||
/** Remaining lifetime below which an indexed file is replaced (default one hour). */
|
||||
fileRefreshMarginSeconds: Volatile<number>
|
||||
/** Oldest harness-owned files deleted before one quota-recovery upload retry (default 100). */
|
||||
fileQuotaCleanupBatch: Volatile<number>
|
||||
/** Provider-owned model-request retry policy; omission uses normal mode with five retries. */
|
||||
retryPolicy: Volatile<RetryPolicyConfig | undefined>
|
||||
}
|
||||
/** Account route configuration; authentication comes exclusively from the account service. */
|
||||
export type Config = ProtocolConfig
|
||||
```
|
||||
|
||||
/** One optional model entry advertised by the direct-fetch adapter. */
|
||||
export interface DeepSeekCatalogModel {
|
||||
/** Wire model id accepted by the configured endpoint. */
|
||||
id: string
|
||||
/** Selector label; defaults to {@link id}. */
|
||||
name?: string
|
||||
/** Optional selector detail for deployments with similar model variants. */
|
||||
description?: string
|
||||
/** Known combined request/response context capacity; omitted when deployment metadata is unavailable. */
|
||||
contextWindow?: number
|
||||
/** Per-request output cap for this model; omission falls back to the profile's {@link DeepSeekConnectionOptions.maxTokens}. */
|
||||
maxTokens?: number
|
||||
/** Accepted request modalities; omission is text-only. */
|
||||
inputModalities?: ModelModality[]
|
||||
/**
|
||||
* Total-pixel budget replacing the published token-grid projection for one
|
||||
* deterministic request preview, or the 512-by-512 `low` preset; omission
|
||||
* projects onto the token grid.
|
||||
*/
|
||||
imagePixelBudget?: number | 'low'
|
||||
/** Encoded-byte target for one deterministic request preview; the smallest quality-ladder output is used when no quality fits. */
|
||||
imageMaxBytes?: number
|
||||
/**
|
||||
* `'in-history'` declares that the endpoint reads the latest `system`
|
||||
* message at any position of the conversation as the complete effective
|
||||
* system prompt; omission means only a leading system message is read.
|
||||
*/
|
||||
systemPromptUpdate?: SystemPromptUpdate
|
||||
依赖: [`ProtocolConfig`](../packages/llm/llm-deepseek/src/index.ts)
|
||||
|
||||
来源: [`packages/llm/llm-deepseek-account/src/config.ts:5`](../packages/llm/llm-deepseek-account/src/config.ts)
|
||||
|
||||
<a id="deepseek-aidsh-llm-deepseek-api-key"></a>
|
||||
|
||||
## `@deepseek-ai/dsh-llm-deepseek-api-key`
|
||||
|
||||
需要: `llm`
|
||||
|
||||
```ts config-catalog
|
||||
/** Messages configuration with a per-request API-key reference. */
|
||||
export interface Config extends ProtocolConfig {
|
||||
/** Credential reference resolved per request; defaults to DEEPSEEK_API_KEY. */
|
||||
apiKeyEnv: Volatile<string>
|
||||
}
|
||||
```
|
||||
|
||||
Depends on: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts) · [`SystemPromptUpdate`](../packages/llm/llm/src/index.ts) · `Volatile` (`@deepseek-ai/cordis`)
|
||||
依赖: [`ProtocolConfig`](../packages/llm/llm-deepseek/src/index.ts) · `Volatile` (`@deepseek-ai/cordis`)
|
||||
|
||||
来源: [`packages/llm/llm-deepseek/src/config.ts:28`](../packages/llm/llm-deepseek/src/config.ts)
|
||||
来源: [`packages/llm/llm-deepseek-api-key/src/config.ts:10`](../packages/llm/llm-deepseek-api-key/src/config.ts)
|
||||
|
||||
<a id="deepseek-aidsh-llm-pi-ai"></a>
|
||||
|
||||
@@ -4249,6 +4191,7 @@ export interface Config {
|
||||
- `@deepseek-ai/dsh-http-proxy`([`packages/util/http-proxy/src/index.ts`](../packages/util/http-proxy/src/index.ts))
|
||||
- `@deepseek-ai/dsh-launch-environment`([`packages/util/launch-environment/src/index.ts`](../packages/util/launch-environment/src/index.ts))
|
||||
- `@deepseek-ai/dsh-lazy-require`([`packages/util/lazy-require/src/index.ts`](../packages/util/lazy-require/src/index.ts))
|
||||
- `@deepseek-ai/dsh-llm-deepseek`([`packages/llm/llm-deepseek/src/index.ts`](../packages/llm/llm-deepseek/src/index.ts))
|
||||
- `@deepseek-ai/dsh-llm-mock-server`([`packages/test-support/llm-mock-server/src/index.ts`](../packages/test-support/llm-mock-server/src/index.ts))
|
||||
- `@deepseek-ai/dsh-loader-smoke`([`packages/test-support/loader-smoke/src/index.ts`](../packages/test-support/loader-smoke/src/index.ts))
|
||||
- `@deepseek-ai/dsh-native-command`([`packages/util/native-command/src/index.ts`](../packages/util/native-command/src/index.ts))
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/event-producer-consumer.md
|
||||
event-producer-consumer.md: 0b66d761bf95950f6b7031552ea08636b8113427
|
||||
event-producer-consumer.zh.md: 5d73788aaff631e7164b335d83f7c0202f2ee9b0
|
||||
event-producer-consumer.md: 9db87ff7725beda6d0aa22c7fea301bab3fd9a5c
|
||||
event-producer-consumer.zh.md: 6d45f01d4cc79d4b746d2f757d05a32d0a2a79ce
|
||||
@@ -9,23 +9,23 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:238`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
|
||||
| `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:59`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` |
|
||||
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:365`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
|
||||
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:263`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:272`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:395`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:301`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
|
||||
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:309`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:322`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:339`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:355`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:282`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
|
||||
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:383`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:610`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:590`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:617`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:596`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:603`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
|
||||
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:393`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
|
||||
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
|
||||
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:381`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:611`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:591`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:618`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:597`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:604`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:45`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) |
|
||||
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
|
||||
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
|
||||
@@ -40,7 +40,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `cordis/request-run-resolved` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:374`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
|
||||
| `credentials/record-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:102`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization), [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform), `remotes` |
|
||||
| `credentials/reference-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:90`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`credentials`](../packages/credentials/credentials), `remotes` |
|
||||
| `deepseek-account/signed-out` | `emit` | [`packages/credentials/deepseek-account/src/index.ts:11`](../packages/credentials/deepseek-account/src/index.ts) | [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) (`emit`) | [`llm-deepseek`](../packages/llm/llm-deepseek) |
|
||||
| `deepseek-account/model-sign-in-required` | `emit` | [`packages/credentials/deepseek-account/src/types.ts:57`](../packages/credentials/deepseek-account/src/types.ts) | [`deepseek-account`](../packages/credentials/deepseek-account) (`emit`) | `remotes` |
|
||||
| `deepseek-account/signed-out` | `emit` | [`packages/credentials/deepseek-account/src/index.ts:12`](../packages/credentials/deepseek-account/src/index.ts) | [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) (`emit`) | [`deepseek-account`](../packages/credentials/deepseek-account) |
|
||||
| `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | [`storage-domain`](../packages/storage/storage-domain), [`workspace`](../packages/workspace/workspace), `workspace-controller` |
|
||||
| `feedback/committed` | `parallel` | [`packages/feedback/message-feedback/src/index.ts:58`](../packages/feedback/message-feedback/src/index.ts) | [`message-feedback`](../packages/feedback/message-feedback) (`parallel`) | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) |
|
||||
| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:67`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
|
||||
|
||||
@@ -11,23 +11,23 @@
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:238`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
|
||||
| `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:59`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` |
|
||||
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:365`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
|
||||
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:263`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:272`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:395`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:301`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
|
||||
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:309`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:322`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:339`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:355`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:282`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
|
||||
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:383`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:610`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:590`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:617`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:596`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:603`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` |
|
||||
| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`schedule`](../packages/schedule/schedule), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:393`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
|
||||
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
|
||||
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:381`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:611`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:591`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:618`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:597`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:604`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:45`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) |
|
||||
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
|
||||
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
|
||||
@@ -42,7 +42,8 @@
|
||||
| `cordis/request-run-resolved` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:374`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
|
||||
| `credentials/record-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:102`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization), [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform), `remotes` |
|
||||
| `credentials/reference-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:90`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`credentials`](../packages/credentials/credentials), `remotes` |
|
||||
| `deepseek-account/signed-out` | `emit` | [`packages/credentials/deepseek-account/src/index.ts:11`](../packages/credentials/deepseek-account/src/index.ts) | [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) (`emit`) | [`llm-deepseek`](../packages/llm/llm-deepseek) |
|
||||
| `deepseek-account/model-sign-in-required` | `emit` | [`packages/credentials/deepseek-account/src/types.ts:57`](../packages/credentials/deepseek-account/src/types.ts) | [`deepseek-account`](../packages/credentials/deepseek-account) (`emit`) | `remotes` |
|
||||
| `deepseek-account/signed-out` | `emit` | [`packages/credentials/deepseek-account/src/index.ts:12`](../packages/credentials/deepseek-account/src/index.ts) | [`deepseek-account-platform`](../packages/credentials/deepseek-account-platform) (`emit`) | [`deepseek-account`](../packages/credentials/deepseek-account) |
|
||||
| `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | [`storage-domain`](../packages/storage/storage-domain), [`workspace`](../packages/workspace/workspace), `workspace-controller` |
|
||||
| `feedback/committed` | `parallel` | [`packages/feedback/message-feedback/src/index.ts:58`](../packages/feedback/message-feedback/src/index.ts) | [`message-feedback`](../packages/feedback/message-feedback) (`parallel`) | [`session-telemetry-otel`](../packages/session/session-telemetry-otel) |
|
||||
| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:67`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/persistence-catalog.md
|
||||
persistence-catalog.md: e8ac36448d2365ae11b103ede84f55499850232e
|
||||
persistence-catalog.zh.md: 1928c824c273fce4ce12016edd2f636234f69fc3
|
||||
persistence-catalog.md: 730e85c773fe34b8fd12a0aff8107401b54620d6
|
||||
persistence-catalog.zh.md: 7829109459c268139f15a804cc2443c6d566360c
|
||||
@@ -5315,7 +5315,7 @@ Array of `number`.
|
||||
|
||||
SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b`
|
||||
|
||||
Sources: [`packages/api/session-controller/src/types.ts:397`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:20`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:20`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:32`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:8`](../packages/experimental/agent-team/src/types.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:12`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/webhook/webhook/src/brand.ts:12`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:6`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:9`](../packages/webhook/webhook/src/brand.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
|
||||
Sources: [`packages/api/session-controller/src/types.ts:398`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:20`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:20`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:32`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:8`](../packages/experimental/agent-team/src/types.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:12`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/webhook/webhook/src/brand.ts:12`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:6`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:9`](../packages/webhook/webhook/src/brand.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
|
||||
|
||||
`string`
|
||||
|
||||
@@ -7248,7 +7248,7 @@ Sources: [`packages/core/tools/src/index.ts:34`](../packages/core/tools/src/inde
|
||||
|
||||
SHA-256: `6a4f72e2e179e17b922f2a9392c0e1c8f8c707f32494372454850a3eb184a6e7`
|
||||
|
||||
Sources: [`packages/api/session-controller/src/types.ts:402`](../packages/api/session-controller/src/types.ts)
|
||||
Sources: [`packages/api/session-controller/src/types.ts:403`](../packages/api/session-controller/src/types.ts)
|
||||
|
||||
| Property | Presence | Type |
|
||||
|---|---|---|
|
||||
|
||||
@@ -5317,7 +5317,7 @@ SHA-256: `5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78`
|
||||
|
||||
SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b`
|
||||
|
||||
来源:[`packages/api/session-controller/src/types.ts:397`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:20`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:20`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:32`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:8`](../packages/experimental/agent-team/src/types.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:12`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/webhook/webhook/src/brand.ts:12`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:6`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:9`](../packages/webhook/webhook/src/brand.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
|
||||
来源:[`packages/api/session-controller/src/types.ts:398`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:20`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:20`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:32`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:8`](../packages/experimental/agent-team/src/types.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:12`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/webhook/webhook/src/brand.ts:12`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:6`](../packages/webhook/webhook/src/brand.ts) · [`packages/webhook/webhook/src/brand.ts:9`](../packages/webhook/webhook/src/brand.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts)
|
||||
|
||||
`string`
|
||||
|
||||
@@ -7250,7 +7250,7 @@ SHA-256: `8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543`
|
||||
|
||||
SHA-256: `6a4f72e2e179e17b922f2a9392c0e1c8f8c707f32494372454850a3eb184a6e7`
|
||||
|
||||
来源:[`packages/api/session-controller/src/types.ts:402`](../packages/api/session-controller/src/types.ts)
|
||||
来源:[`packages/api/session-controller/src/types.ts:403`](../packages/api/session-controller/src/types.ts)
|
||||
|
||||
| 属性 | 存在性 | 类型 |
|
||||
|---|---|---|
|
||||
|
||||
Generated
+2
-2
@@ -44608,7 +44608,7 @@
|
||||
"packages/workflow/workflow/src/types.ts#WorkflowRunId"
|
||||
],
|
||||
"sources": [
|
||||
"packages/api/session-controller/src/types.ts:397",
|
||||
"packages/api/session-controller/src/types.ts:398",
|
||||
"packages/attachment/attachment/src/brand.ts:6",
|
||||
"packages/compaction/compaction/src/brand.ts:4",
|
||||
"packages/core/session/src/types.ts:20",
|
||||
@@ -58191,7 +58191,7 @@
|
||||
},
|
||||
"names": [],
|
||||
"sources": [
|
||||
"packages/api/session-controller/src/types.ts:402"
|
||||
"packages/api/session-controller/src/types.ts:403"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/subsystems/core.md
|
||||
core.md: d6069514aebceb1b226874dc55a1711ee92adbdc
|
||||
core.zh.md: eb6a4b66509cd572618bcab85f8cb520ee8ddea3
|
||||
core.md: 36fee2dfea5e38229f16ddbca2da567eeca0f2c5
|
||||
core.zh.md: c807828249fde64df291ca098922829107515d1d
|
||||
@@ -69,8 +69,6 @@ interface Agent {
|
||||
readonly inbox: Inbox
|
||||
/** The current lifecycle state, mirrored on every `agent/status` transition. */
|
||||
readonly status: AgentStatus
|
||||
/** Latest prepared provider in this turn; retained through tools and retries, absent before preparation and between turns. */
|
||||
readonly activeProvider: string | undefined
|
||||
/** Agent-scoped context; its contributions are agent-local, unwind on disposal, and reject registration afterward. */
|
||||
readonly ctx: Context
|
||||
|
||||
@@ -1105,5 +1103,3 @@ One session committed a different agent preset to its durable log. Consumers inv
|
||||
|
||||
Source: [`packages/preset/agent-preset-registry/src/types.ts`](../../packages/preset/agent-preset-registry/src/types.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
|
||||
`Agent.activeProvider` is the latest prepared request’s provider in the current turn. It is undefined before first preparation and between turns, stays unchanged during tools and subsequent preparation, and changes only when the next final request configuration is bound.
|
||||
@@ -73,8 +73,6 @@ interface Agent {
|
||||
readonly inbox: Inbox
|
||||
/** The current lifecycle state, mirrored on every `agent/status` transition. */
|
||||
readonly status: AgentStatus
|
||||
/** Latest prepared provider in this turn; retained through tools and retries, absent before preparation and between turns. */
|
||||
readonly activeProvider: string | undefined
|
||||
/** Agent-scoped context; its contributions are agent-local, unwind on disposal, and reject registration afterward. */
|
||||
readonly ctx: Context
|
||||
|
||||
@@ -1115,5 +1113,3 @@ One session committed a different agent preset to its durable log. Consumers inv
|
||||
|
||||
Source: [`packages/preset/agent-preset-registry/src/types.ts`](../../packages/preset/agent-preset-registry/src/types.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
|
||||
`Agent.activeProvider` 是当前轮次最近已准备请求的提供方。首请求准备前及轮次之间为 undefined,工具执行和后续请求准备期间保持不变,仅在绑定下一最终请求配置时改变。
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/subsystems/credentials.md
|
||||
credentials.md: f6d510d48ec92feb28cc3af8820a1e497fad2cdd
|
||||
credentials.zh.md: 7040fe28cb930a0f2dce9a092124fa24e72cf767
|
||||
credentials.md: 46427bd45c50a9ecee173ba05528339692abbb7f
|
||||
credentials.zh.md: 1c3e6f3d16e08fe06376f77ff79df3ffcc208cde
|
||||
@@ -414,6 +414,21 @@ Source: [`packages/credentials/credentials/src/types.ts`](../../packages/credent
|
||||
|
||||
### `deepseek-account/*` events
|
||||
|
||||
<a id="deepseek-accountmodel-sign-in-required--emit"></a>
|
||||
|
||||
#### `deepseek-account/model-sign-in-required` — emit
|
||||
|
||||
An account model request requires the user to sign in.
|
||||
|
||||
```ts cordis-catalog
|
||||
/** An account model request requires the user to sign in.
|
||||
* @mode emit
|
||||
*/
|
||||
'deepseek-account/model-sign-in-required'(): void
|
||||
```
|
||||
|
||||
Source: [`packages/credentials/deepseek-account/src/types.ts`](../../packages/credentials/deepseek-account/src/types.ts)
|
||||
|
||||
<a id="deepseek-accountsigned-out--emit"></a>
|
||||
|
||||
#### `deepseek-account/signed-out` — emit
|
||||
|
||||
@@ -414,6 +414,21 @@ Source: [`packages/credentials/credentials/src/types.ts`](../../packages/credent
|
||||
|
||||
### `deepseek-account/*` events
|
||||
|
||||
<a id="deepseek-accountmodel-sign-in-required--emit"></a>
|
||||
|
||||
#### `deepseek-account/model-sign-in-required` — emit
|
||||
|
||||
An account model request requires the user to sign in.
|
||||
|
||||
```ts cordis-catalog
|
||||
/** An account model request requires the user to sign in.
|
||||
* @mode emit
|
||||
*/
|
||||
'deepseek-account/model-sign-in-required'(): void
|
||||
```
|
||||
|
||||
Source: [`packages/credentials/deepseek-account/src/types.ts`](../../packages/credentials/deepseek-account/src/types.ts)
|
||||
|
||||
<a id="deepseek-accountsigned-out--emit"></a>
|
||||
|
||||
#### `deepseek-account/signed-out` — emit
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/subsystems/session.md
|
||||
session.md: 8a8fbc6a68b414b9e8d658822bc9f2cb26000d8f
|
||||
session.zh.md: 04a4ed2becfad588bc8c1fcd64cba5d84820ea5a
|
||||
session.md: 8c060b1a34f9f16ebf0ececd28ecdf24e77f497b
|
||||
session.zh.md: dc26e5a5dc15251bef1fb93cc53ceff2fee55ab6
|
||||
@@ -821,9 +821,9 @@ inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<SessionInspectio
|
||||
@Remote('selectModel') selectModel(request: SessionSelectModelRequest): Promise<SessionSelectModelValue>
|
||||
|
||||
/**
|
||||
* Initialize the default after the user first configures a provider credential.
|
||||
* Initialize a provider default; account login replaces it only when no provider API key is configured.
|
||||
* @param provider - the provider whose credential was configured.
|
||||
* @returns after saving the initial available model; existing user choices are retained.
|
||||
* @returns after saving the first available model or retaining the existing default.
|
||||
*/
|
||||
@Remote async initializeDefaultModel(provider: string): Promise<void>
|
||||
|
||||
|
||||
@@ -825,9 +825,9 @@ inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<SessionInspectio
|
||||
@Remote('selectModel') selectModel(request: SessionSelectModelRequest): Promise<SessionSelectModelValue>
|
||||
|
||||
/**
|
||||
* Initialize the default after the user first configures a provider credential.
|
||||
* Initialize a provider default; account login replaces it only when no provider API key is configured.
|
||||
* @param provider - the provider whose credential was configured.
|
||||
* @returns after saving the initial available model; existing user choices are retained.
|
||||
* @returns after saving the first available model or retaining the existing default.
|
||||
*/
|
||||
@Remote async initializeDefaultModel(provider: string): Promise<void>
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/api/account-controller/README.md
|
||||
README.md: e1d0769e5e76541355f6b725627a833ce399fa2b
|
||||
README.zh.md: cbc167df735d692c5395bf167c777f1bf17fe8a5
|
||||
README.md: a5d3405353245335a45a26eedc4e4281cf045ecb
|
||||
README.zh.md: 353d79fac4ab0a01a659d9f1ad9d2944fe01f735
|
||||
@@ -22,7 +22,7 @@ Account screens use authenticated Remote commands and a snapshot stream. The con
|
||||
|
||||
The account namespace exposes getState, getProfile / getBalance, startSignIn, cancelSignIn, signOut, and watch. watch emits an initial complete state and subsequent complete states; disconnecting stops observation, not the login attempt. Cancellation names the attempt ID so a stale screen cannot cancel a newer login.
|
||||
|
||||
hasRunningAccountTasks reads the agents registry and reports whether any active prepared provider is deepseek-account, including tool execution and retries. It excludes unprepared turns and API-key providers. The result describes the query instant; sign-out independently cancels matching agents when credentials are removed.
|
||||
`hasRunningAccountTasks` checks running Agents through the account-owned predicate over their latest logged request context, including tools and retries. Idle Agents and API-key contexts are excluded. The account provider independently cancels matching tasks when credentials are removed.
|
||||
|
||||
<a id="understand-the-implementation"></a>
|
||||
## Understand the implementation
|
||||
|
||||
@@ -22,7 +22,7 @@ kind: "package-reference"
|
||||
|
||||
account 命名空间提供 getState、getProfile / getBalance、startSignIn、cancelSignIn、signOut 和 watch。watch 先发送完整初始状态,随后发送完整状态变化;断开连接只停止观察,不取消登录。取消操作必须指定尝试 ID,防止旧页面取消新登录。
|
||||
|
||||
hasRunningAccountTasks 读取 agents 注册表,判断是否有任务实际使用 deepseek-account,包括工具执行和重试阶段;未完成请求准备的轮次和 API key provider 不计入。结果反映查询时刻;移除凭据时,退登操作会独立取消匹配的 Agent。
|
||||
`hasRunningAccountTasks` 通过账号模块的判断函数,检查运行中 Agent 最近记录的请求上下文,包括工具和重试阶段。空闲 Agent 及 API key 上下文不计入。移除凭据时,账号提供方独立取消匹配任务。
|
||||
|
||||
<a id="understand-the-implementation"></a>
|
||||
## 理解实现
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** Authenticated Remote operations for account UI consumers. */
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type {} from '@deepseek-ai/dsh-deepseek-account'
|
||||
import { isRunningAccountTask } from '@deepseek-ai/dsh-deepseek-account'
|
||||
import type {} from '@deepseek-ai/dsh-agent'
|
||||
import type { AccountDetails } from '@deepseek-ai/dsh-deepseek-account/types'
|
||||
import type { AccountView, SignInAttemptId } from './types.ts'
|
||||
@@ -46,12 +46,12 @@ export class AccountController extends TypertRemoteService {
|
||||
@Remote
|
||||
cancelSignIn(attemptId: SignInAttemptId): Promise<AccountView> { return this.ctx.deepseekAccount.cancelSignIn(attemptId) }
|
||||
/**
|
||||
* Inspect prepared providers across running tasks, including tools and retries.
|
||||
* @returns whether any task currently uses the account token; unprepared turns are excluded.
|
||||
* Inspect the latest logged request providers of running tasks, including tools and retries.
|
||||
* @returns whether running work has a latest request context on the account route.
|
||||
*/
|
||||
@Remote
|
||||
hasRunningAccountTasks(): boolean {
|
||||
return this.ctx.agents.list().some(agent => agent.activeProvider === 'deepseek-account')
|
||||
return this.ctx.agents.list().some(isRunningAccountTask)
|
||||
}
|
||||
/**
|
||||
* Remove the local account grant and revoke it through Platform in the background, without deleting API keys.
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
/** Sign-out impact reads prepared providers rather than configured routes. */
|
||||
/** Sign-out impact follows the latest logged provider of running tasks. */
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { expect, it } from 'vitest'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { AccountController } from '../src/index.ts'
|
||||
|
||||
it('reports only currently prepared account-token tasks', async () => {
|
||||
it('reports running account requests and excludes idle or unbound tasks', async () => {
|
||||
const ctx = new Context()
|
||||
const active: Array<Pick<Agent, 'activeProvider'>> = []
|
||||
// This query only consumes the registry list and each agent's prepared provider.
|
||||
ctx.provide('agents')
|
||||
ctx.set('agents', { list: () => active } as Context['agents'])
|
||||
const active: Array<Pick<Agent, 'status' | 'session'>> = []
|
||||
const task = (status: Agent['status'], provider?: string): Pick<Agent, 'status' | 'session'> => ({
|
||||
status, session: { requestContext: () => provider === undefined ? undefined : { provider, model: 'model' } } as Agent['session'],
|
||||
})
|
||||
ctx.provide('agents', { list: () => active } as Context['agents'])
|
||||
const controller = new AccountController(ctx)
|
||||
try {
|
||||
expect(controller.hasRunningAccountTasks()).toBe(false)
|
||||
active.push({ activeProvider: undefined }, { activeProvider: 'deepseek-official' })
|
||||
active.push(task('running'), task('running', 'deepseek-official'), task('idle', 'deepseek-account'))
|
||||
expect(controller.hasRunningAccountTasks()).toBe(false)
|
||||
active.push({ activeProvider: 'deepseek-account' })
|
||||
active.push(task('running', 'deepseek-account'))
|
||||
expect(controller.hasRunningAccountTasks()).toBe(true)
|
||||
active.pop()
|
||||
expect(controller.hasRunningAccountTasks()).toBe(false)
|
||||
|
||||
@@ -21,7 +21,6 @@ export async function registerAgent(ctx: Context, rawId: string): Promise<Agent
|
||||
const id = SessionId(rawId)
|
||||
const session = Session.create(id)
|
||||
const agent: Agent & { disposeScope: () => Promise<void> } = {
|
||||
activeProvider: undefined,
|
||||
id,
|
||||
options: {},
|
||||
session,
|
||||
|
||||
@@ -226,7 +226,6 @@ describe('JobController', () => {
|
||||
async function liveOwner(ctx: Context, rawId: string): Promise<Agent> {
|
||||
const session = ctx.sessions.create(SessionId(rawId))
|
||||
const owner: Agent = {
|
||||
activeProvider: undefined,
|
||||
id: session.id,
|
||||
options: {},
|
||||
session,
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
"@deepseek-ai/dsh-plugin-manager": "workspace:*",
|
||||
"@deepseek-ai/dsh-client-ui-plugin-manager": "workspace:*",
|
||||
"@deepseek-ai/dsh-office-to-pdf": "workspace:*",
|
||||
"@deepseek-ai/dsh-api-account-controller": "workspace:*"
|
||||
"@deepseek-ai/dsh-api-account-controller": "workspace:*",
|
||||
"@deepseek-ai/dsh-deepseek-account": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import type {} from '@deepseek-ai/dsh-api-session-controller/remote-events'
|
||||
import type {} from '@deepseek-ai/dsh-deepseek-account/types'
|
||||
import type {} from '@deepseek-ai/dsh-permission-presets/types'
|
||||
import type {} from '@deepseek-ai/dsh-plugin-manager/types'
|
||||
import type { TypertForwardableEventEntry } from '@deepseek-ai/dsh-typert-protocol'
|
||||
@@ -24,6 +25,7 @@ export const API_REMOTE_FORWARDED_EVENTS = [
|
||||
{ event: 'api-session/removed', mode: 'emit' },
|
||||
{ event: 'api-session/status', mode: 'emit' },
|
||||
{ event: 'commands/change', mode: 'emit' },
|
||||
{ event: 'deepseek-account/model-sign-in-required', mode: 'emit' },
|
||||
{ event: 'credentials/record-updated', mode: 'emit' },
|
||||
{ event: 'credentials/reference-updated', mode: 'emit' },
|
||||
{ event: 'goal/activation-changed', mode: 'emit' },
|
||||
|
||||
@@ -100,6 +100,9 @@
|
||||
},
|
||||
{
|
||||
"path": "../account-controller"
|
||||
},
|
||||
{
|
||||
"path": "../../credentials/deepseek-account"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -82,6 +82,9 @@
|
||||
},
|
||||
{
|
||||
"path": "../account-controller"
|
||||
},
|
||||
{
|
||||
"path": "../../credentials/deepseek-account"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/api/session-controller/README.md
|
||||
README.md: 38ee7d644784344e95745d8ddf8d2efc48f18110
|
||||
README.zh.md: 75625d404ac68430c80c966c928a080a22b174e3
|
||||
README.md: abe79ff7b2b4be6f2b0ba626c77ad9af22fa2f33
|
||||
README.zh.md: 7468ed80cb741574a8ab79422ee363af2bbad3e4
|
||||
@@ -78,7 +78,7 @@ References keep local Session data, scoped Contexts, and history streams alive,
|
||||
|
||||
`SessionMediaReferences` mounts `GET|HEAD /api/file?path=<absolute path>` on the authenticated `connection.fetch` channel when `connection`, `fs`, and `attachments` are composed. It reads ordinary files through `ctx.fs`, including temporary paths outside registered workspaces and files in remote providers. Neither directory containment nor MIME categories restrict access; `mime-types` supplies the response type, with `application/octet-stream` for unknown extensions. GET reuses `readBytes` for preflight and ongoing byte limits; HEAD reads metadata only. All files use `ctx.attachments.imageLimits.maxImageBytes` (normally 20 MiB); exceeding this limit returns 413. Responses contain the complete file, ignore Range, and carry `private, no-store`, `nosniff`, and a sandbox CSP so directly opened HTML/SVG cannot execute with the API origin. The Client rewrite lives in `ui-chat` (`AssistantMarkdown`); audio/video responses are available, while Markdown audio/video player nodes remain separate work.
|
||||
|
||||
GUI model selection and prompt admission require the exact provider/model pair in the current available catalog. An unavailable selection or a failed catalog query rejects with `session/model-unavailable` before delivery; admission checks again after attachment preparation with the same error code. `initializeDefaultModel(provider)` saves the first available model of the explicitly configured provider only before any user choice exists. A provider with no available models rejects initialization with `session/provider-models-unavailable`. Availability never substitutes another model or rewrites a Session selection. These catalog requirements belong to GUI admission; direct LLM calls can use uncatalogued models.
|
||||
GUI model selection requires the exact provider/model pair in the available catalog; unavailable choices reject with `session/model-unavailable`. Prompt admission retains the saved route without a catalog availability gate, so request execution reports missing credentials or unavailable models. `initializeDefaultModel(provider)` saves the first available model of an API-key provider only before any user choice exists. Account login instead replaces the default when no other provider has a configured API key; credential checks use configured references independently of model availability. A provider with no available models rejects initialization with `session/provider-models-unavailable`. Availability never substitutes another model or rewrites a Session selection.
|
||||
|
||||
-----
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ Client 的首次 `follow`、重连首屏与 `loadOlder()` 至少请求 50 条以
|
||||
|
||||
当 `connection`、`fs` 与 `attachments` 均被组合时,`SessionMediaReferences` 在鉴权 `connection.fetch` 通道上挂载 `GET|HEAD /api/file?path=<绝对路径>`。它通过 `ctx.fs` 读取普通文件,包括已注册工作区之外的临时路径与远程提供方中的文件。目录包含关系与 MIME 类别均不限制访问;`mime-types` 提供响应类型,未知扩展名使用 `application/octet-stream`。GET 复用 `readBytes` 执行读取前及读取中的字节限制;HEAD 只读取元数据。所有文件均使用 `ctx.attachments.imageLimits.maxImageBytes`(通常为 20 MiB);超过此上限返回 413。响应包含完整文件,忽略 Range,并携带 `private, no-store`、`nosniff` 与沙箱 CSP,使直接打开的 HTML/SVG 无法以 API 源身份执行脚本。客户端重写位于 `ui-chat`(`AssistantMarkdown`);音视频文件响应已可用,Markdown 音视频播放器节点仍是独立工作。
|
||||
|
||||
GUI 模型选择与提示词准入要求确切提供方/模型对出现在当前可用目录中。选择不可用或目录查询失败时,投递前以 `session/model-unavailable` 拒绝;附件准备完成后再次检查,并使用相同错误码。`initializeDefaultModel(provider)` 仅在用户尚未选择时,保存其显式配置的提供方的第一个可用模型。提供方没有可用模型时,初始化以 `session/provider-models-unavailable` 拒绝。可用性变化不会替换模型或改写会话选择。这些目录要求属于 GUI 准入;直接 LLM 调用可使用未列入目录的模型。
|
||||
GUI 模型选择要求确切提供方/模型对出现在可用目录中;不可用的选择以 `session/model-unavailable` 拒绝。提示词准入保留已保存的路由,不按目录可用性阻断发送,由请求执行报告凭据缺失或模型不可用。`initializeDefaultModel(provider)` 仅在用户尚未选择时,保存 API key 提供方的第一个可用模型。账号登录则在其他提供方均未配置 API key 时替换默认模型;凭据检查使用已配置的引用,不依赖模型是否可用。提供方没有可用模型时,初始化以 `session/provider-models-unavailable` 拒绝。可用性变化不会替换模型或改写会话选择。
|
||||
|
||||
-----
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
"@deepseek-ai/dsh-client-connection": "workspace:*",
|
||||
"@deepseek-ai/dsh-client-file-upload": "workspace:*",
|
||||
"@deepseek-ai/dsh-commands": "workspace:*",
|
||||
"@deepseek-ai/dsh-credentials": "workspace:*",
|
||||
"@deepseek-ai/dsh-file-reference": "workspace:*",
|
||||
"@deepseek-ai/dsh-fs": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm": "workspace:*",
|
||||
@@ -96,6 +97,7 @@
|
||||
"@deepseek-ai/dsh-session-projection-cache": "workspace:*",
|
||||
"@deepseek-ai/dsh-session-query": "workspace:*",
|
||||
"@deepseek-ai/dsh-session-title": "workspace:*",
|
||||
"@deepseek-ai/dsh-settings": "workspace:*",
|
||||
"@deepseek-ai/dsh-skill": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent": "workspace:*",
|
||||
"@deepseek-ai/dsh-typert-protocol": "workspace:*",
|
||||
@@ -111,6 +113,9 @@
|
||||
},
|
||||
"@deepseek-ai/dsh-session-projection-cache": {
|
||||
"optional": true
|
||||
},
|
||||
"@deepseek-ai/dsh-settings": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -127,6 +132,7 @@
|
||||
"@deepseek-ai/dsh-client-store": "workspace:*",
|
||||
"@deepseek-ai/dsh-client-test-runtime": "workspace:*",
|
||||
"@deepseek-ai/dsh-commands": "workspace:*",
|
||||
"@deepseek-ai/dsh-credentials": "workspace:*",
|
||||
"@deepseek-ai/dsh-file-reference": "workspace:*",
|
||||
"@deepseek-ai/dsh-fs": "workspace:*",
|
||||
"@deepseek-ai/dsh-fs-local": "workspace:*",
|
||||
@@ -141,6 +147,7 @@
|
||||
"@deepseek-ai/dsh-session-projection-cache": "workspace:*",
|
||||
"@deepseek-ai/dsh-session-query": "workspace:*",
|
||||
"@deepseek-ai/dsh-session-title": "workspace:*",
|
||||
"@deepseek-ai/dsh-settings": "workspace:*",
|
||||
"@deepseek-ai/dsh-skill": "workspace:*",
|
||||
"@deepseek-ai/dsh-storage": "workspace:*",
|
||||
"@deepseek-ai/dsh-storage-domain": "workspace:*",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/** Shared projection of the live LLM registry into the browser model catalog. */
|
||||
|
||||
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
||||
import type {} from '@deepseek-ai/dsh-settings'
|
||||
import type { LlmModelInfo } from '@deepseek-ai/dsh-llm'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type {
|
||||
@@ -86,3 +88,29 @@ export async function modelAvailable(ctx: Context, selection: ModelSelection): P
|
||||
}
|
||||
return models.some(model => model.id === selection.model)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check configured provider API-key references independently of model availability.
|
||||
* @param ctx - Host registry, settings, and credential services.
|
||||
* @returns whether any API-key provider has a configured credential.
|
||||
*/
|
||||
export async function hasProviderApiKey(ctx: Context): Promise<boolean> {
|
||||
const settings = ctx.get('settings')
|
||||
const credentials = ctx.get('credentials')
|
||||
if (settings === undefined || credentials === undefined) {
|
||||
throw new RemoteError('session/provider-credentials-unavailable', 'provider credentials are unavailable', {})
|
||||
}
|
||||
const namespaces = settings.describe({ redactSecrets: true })
|
||||
for (const provider of ctx.llm.listConfigurableProviders()) {
|
||||
if (provider.provider === 'deepseek-account') continue
|
||||
let profile = namespaces.find(namespace => namespace.ns === provider.settingsNs)?.value
|
||||
for (const key of provider.settingsPath) {
|
||||
profile = typeof profile === 'object' && profile !== null ? Reflect.get(profile, key) : undefined
|
||||
}
|
||||
if (typeof profile !== 'object' || profile === null) continue
|
||||
const ref: unknown = Reflect.get(profile, 'apiKeyEnv')
|
||||
if (typeof ref === 'string' && ref.length > 0
|
||||
&& (await credentials.describe(credentialRef(ref))).configured) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -330,8 +330,6 @@ export class SessionCommandController {
|
||||
}
|
||||
const agent = await this.resolveAgent(request.sessionId)
|
||||
if (hasPromptRequest(agent, request.requestId)) return { accepted: true }
|
||||
const selection = this.agents.selectionFor(agent).current
|
||||
await this.requireModel(selection)
|
||||
const source: MessageSource = {
|
||||
kind: 'user',
|
||||
rpcId: request.requestId,
|
||||
@@ -357,7 +355,6 @@ export class SessionCommandController {
|
||||
)
|
||||
const content = await this.ctx.attachments.admitPromptContent(admission.content)
|
||||
const message: UserMessage = createUserMessage({ content, source })
|
||||
await this.requireModel(this.agents.selectionFor(agent).current)
|
||||
if (this.ctx.agents.get(agent.id) !== agent) {
|
||||
throw new RemoteError(
|
||||
'session/not-found',
|
||||
|
||||
@@ -23,7 +23,7 @@ import { SessionControlController } from './control.ts'
|
||||
import { SessionHistoryController } from './history.ts'
|
||||
import { SessionFileReferences } from './file-references.ts'
|
||||
import { ApiSessionList } from './list.ts'
|
||||
import { buildModelCatalog } from './catalog.ts'
|
||||
import { buildModelCatalog, hasProviderApiKey } from './catalog.ts'
|
||||
import { installModelSelectionProjection } from './model-selection-projection.ts'
|
||||
import { SessionSkillCatalog } from './skill-catalog.ts'
|
||||
import { SessionMediaReferences } from './media-references.ts'
|
||||
@@ -285,19 +285,22 @@ export class SessionController extends TypertRemoteService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the default after the user first configures a provider credential.
|
||||
* Initialize a provider default; account login replaces it only when no provider API key is configured.
|
||||
* @param provider - the provider whose credential was configured.
|
||||
* @returns after saving the initial available model; existing user choices are retained.
|
||||
* @returns after saving the first available model or retaining the existing default.
|
||||
*/
|
||||
@Remote
|
||||
async initializeDefaultModel(provider: string): Promise<void> {
|
||||
if (provider === 'deepseek-account' && await hasProviderApiKey(this.ctx)) return
|
||||
const catalog = await buildModelCatalog(this.ctx)
|
||||
const model = catalog.groups.find(group => group.id === provider)?.models[0]
|
||||
if (model === undefined) throw new RemoteError('session/provider-models-unavailable',
|
||||
`provider "${provider}" has no available models`, { provider })
|
||||
await this.ctx.agentDefaultModel.initializeSelection({ provider, model: model.id,
|
||||
const selection = { provider, model: model.id,
|
||||
...model.reasoning?.defaultEffort === undefined ? {} : { reasoningEffort: ReasoningEffortId(model.reasoning.defaultEffort) },
|
||||
})
|
||||
}
|
||||
if (provider === 'deepseek-account') await this.ctx.agentDefaultModel.saveSelection(selection)
|
||||
else await this.ctx.agentDefaultModel.initializeSelection(selection)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -201,6 +201,7 @@ export const SESSION_SEARCH_SNIPPET_MAX_CODE_POINTS = 240
|
||||
|
||||
declare module '@deepseek-ai/dsh-typert-protocol' {
|
||||
interface RemoteErrorDetailsMap {
|
||||
'session/provider-credentials-unavailable': Record<string, never>
|
||||
'session/provider-models-unavailable': { readonly provider: string }
|
||||
'session/model-unavailable': { readonly provider: string; readonly model: string }
|
||||
'session/conflict': {
|
||||
|
||||
@@ -26,7 +26,6 @@ async function harness(): Promise<Harness> {
|
||||
/** A registered idle Agent double: the gate reads only its Session header. */
|
||||
async function liveAgent(ctx: Context, session: Session): Promise<Agent> {
|
||||
const agent: Agent = {
|
||||
activeProvider: undefined,
|
||||
id: session.id,
|
||||
options: {},
|
||||
session,
|
||||
|
||||
@@ -20,7 +20,7 @@ import SessionStore from '@deepseek-ai/dsh-session'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionPromptRequest, SessionRequestId } from '../src/types.ts'
|
||||
import { ApiSessionAgentController } from '../src/agent.ts'
|
||||
import { buildModelCatalog } from '../src/catalog.ts'
|
||||
import { buildModelCatalog, hasProviderApiKey } from '../src/catalog.ts'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { createSessionTestController, createSessionTestRemote } from './test-remote.ts'
|
||||
@@ -365,6 +365,66 @@ describe('Web session model selection', () => {
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('checks configured key references even for empty catalogs and skips profiles without keys', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmRuntime)
|
||||
const describe = vi.fn(async () => ({ configured: true, writable: true }))
|
||||
ctx.provide('credentials', { describe } as never)
|
||||
ctx.provide('settings', { describe: () => [{ ns: 'profiles', value: {
|
||||
providers: { blank: { apiKeyEnv: '' }, absent: {}, configured: { apiKeyEnv: 'CUSTOM_KEY' } },
|
||||
} }] } as never)
|
||||
const routes = ['missing', 'blank', 'absent', 'configured']
|
||||
ctx.effect(() => ctx.llm.registerConfigurableProviders(routes.map(provider => ({
|
||||
provider, displayName: provider, settingsNs: 'profiles',
|
||||
settingsPath: provider === 'missing' ? ['missing', 'nested'] : ['providers', provider],
|
||||
}))))
|
||||
try {
|
||||
expect(await hasProviderApiKey(ctx)).toBe(true)
|
||||
expect(describe).toHaveBeenCalledExactlyOnceWith('CUSTOM_KEY')
|
||||
describe.mockRejectedValueOnce(new Error('credential read failed'))
|
||||
await expect(hasProviderApiKey(ctx)).rejects.toThrow('credential read failed')
|
||||
} finally { await ctx.fiber.dispose() }
|
||||
})
|
||||
|
||||
it.each(['settings', 'credentials'] as const)('refuses account initialization without %s inspection', async (missing) => {
|
||||
const ctx = new Context()
|
||||
if (missing !== 'settings') ctx.provide('settings', {} as never)
|
||||
if (missing !== 'credentials') ctx.provide('credentials', {} as never)
|
||||
try {
|
||||
await expect(hasProviderApiKey(ctx)).rejects.toMatchObject({ code: 'session/provider-credentials-unavailable' })
|
||||
} finally { await ctx.fiber.dispose() }
|
||||
})
|
||||
|
||||
it.each([false, true])('account login replaces a saved default only without another API key: %s', async (configuredKey) => {
|
||||
const { configurationFixture } = await import('../../../settings/settings/tests/configuration-fixture.ts')
|
||||
const configured = await configurationFixture({ hmr: false })
|
||||
const { ctx } = await harness(undefined, configured.ctx)
|
||||
ctx.effect(() => ctx.llm.registerAdapter(['deepseek-account'], new CatalogAdapter('Account', [
|
||||
{ provider: 'deepseek-account', id: 'first-model', name: 'First' },
|
||||
{ provider: 'deepseek-account', id: 'second-model', name: 'Second' },
|
||||
], REASONING)))
|
||||
const describe = vi.fn(async () => ({ configured: configuredKey, writable: true }))
|
||||
ctx.provide('credentials', { describe } as never)
|
||||
ctx.effect(() => ctx.llm.registerConfigurableProviders([
|
||||
{ provider: 'deepseek-account', displayName: 'Account', settingsNs: 'account', settingsPath: [] },
|
||||
{ provider: 'removed-model-provider', displayName: 'Custom', settingsNs: 'first', settingsPath: ['providers', 'custom'] },
|
||||
]))
|
||||
vi.spyOn(ctx.settings, 'describe').mockReturnValue([{
|
||||
ns: 'first' as never, autoGenerate: false, schema: {}, revision: 0, applies: 'live',
|
||||
value: { providers: { custom: { apiKeyEnv: 'CUSTOM_API_KEY', models: [] } } },
|
||||
}])
|
||||
await ctx.agentDefaultModel.saveSelection({ provider: 'removed', model: 'saved' })
|
||||
const controller = createSessionTestController(ctx, {
|
||||
defaultModelSelection: () => ctx.agentDefaultModel.currentSelection(), cwd: '/tmp',
|
||||
})
|
||||
await controller.initializeDefaultModel('deepseek-account')
|
||||
expect(describe).toHaveBeenCalledWith('CUSTOM_API_KEY')
|
||||
expect(ctx.agentDefaultModel.currentSelection()).toEqual(configuredKey
|
||||
? { provider: 'removed', model: 'saved' }
|
||||
: { provider: 'deepseek-account', model: 'first-model', reasoningEffort: 'high' })
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('groups successful providers and leaves an unlisted current selection out of the catalog', async () => {
|
||||
const { ctx, sessionId } = await harness({
|
||||
provider: 'deepseek-official',
|
||||
@@ -613,7 +673,7 @@ describe('Web session model selection', () => {
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it.each([1, 2])('reports catalog errors as model-unavailable at prompt check %s', async (check) => {
|
||||
it('admits saved selections without querying the advisory model catalog', async () => {
|
||||
const { ctx, sessionId, agent } = await harness()
|
||||
const followup = vi.fn()
|
||||
Object.assign(agent, { followup })
|
||||
@@ -621,17 +681,13 @@ describe('Web session model selection', () => {
|
||||
defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }), cwd: '/tmp',
|
||||
})
|
||||
const list = vi.spyOn(ctx.llm, 'listModels')
|
||||
if (check === 2) list.mockResolvedValueOnce([{ provider: 'deepseek-official', id: 'deepseek-chat', name: 'Chat' }])
|
||||
list.mockRejectedValueOnce(new Error('credential storage offline'))
|
||||
try {
|
||||
expect(await remote.prompt(promptRequest({
|
||||
sessionId, mode: 'queue', content: [{ type: 'text', text: 'hello' }],
|
||||
}))).toMatchObject({ ok: false, error: {
|
||||
code: 'session/model-unavailable', message: 'credential storage offline',
|
||||
details: { provider: 'deepseek-official', model: 'deepseek-chat' },
|
||||
} })
|
||||
expect(list).toHaveBeenCalledTimes(check)
|
||||
expect(followup).not.toHaveBeenCalled()
|
||||
}))).toMatchObject({ ok: true, value: { accepted: true } })
|
||||
expect(list).not.toHaveBeenCalled()
|
||||
expect(followup).toHaveBeenCalledOnce()
|
||||
} finally {
|
||||
list.mockRestore()
|
||||
await ctx.fiber.dispose()
|
||||
@@ -655,22 +711,20 @@ describe('Web session model selection', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('refuses a prompt no adapter can route, and reports it on the directory', async () => {
|
||||
const { ctx, sessionId } = await harness()
|
||||
it('admits an unavailable saved route for execution without rewriting the selection', async () => {
|
||||
const { ctx, sessionId, agent } = await harness()
|
||||
const followup = vi.fn()
|
||||
Object.assign(agent, { followup })
|
||||
const remote = createSessionTestRemote(ctx, {
|
||||
defaultModelSelection: () => ({ provider: 'deleted-gateway', model: 'deleted-model' }),
|
||||
cwd: '/tmp',
|
||||
})
|
||||
|
||||
// The client disabling its input is an affordance; this method stays
|
||||
// callable, so the refusal has to live here.
|
||||
const refused = await remote.prompt(promptRequest({
|
||||
const admitted = await remote.prompt(promptRequest({
|
||||
sessionId, mode: 'queue' as const, content: [{ type: 'text' as const, text: 'hi' }],
|
||||
}))
|
||||
expect(refused).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: 'session/model-unavailable', details: { provider: 'deleted-gateway', model: 'deleted-model' } },
|
||||
})
|
||||
expect(admitted).toMatchObject({ ok: true, value: { accepted: true } })
|
||||
expect(followup).toHaveBeenCalledOnce()
|
||||
const unavailableCatalog = await buildModelCatalog(ctx)
|
||||
expect(unavailableCatalog.routableProviders.includes(currentSelection(ctx, sessionId).provider)).toBe(false)
|
||||
|
||||
@@ -681,14 +735,17 @@ describe('Web session model selection', () => {
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('refuses a removed model while its provider still serves other models without changing the log', async () => {
|
||||
it('admits a removed model without changing its saved selection', async () => {
|
||||
const { ctx, sessionId, agent } = await harness({ provider: 'deepseek-official', model: 'removed-model' })
|
||||
const remote = createSessionTestRemote(ctx, {
|
||||
defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }), cwd: '/tmp',
|
||||
})
|
||||
const events = [...agent.session.snapshotEvents()]
|
||||
const followup = vi.fn()
|
||||
Object.assign(agent, { followup })
|
||||
expect(await remote.prompt(promptRequest({ sessionId, mode: 'queue', content: [{ type: 'text', text: 'hi' }] })))
|
||||
.toMatchObject({ ok: false, error: { code: 'session/model-unavailable' } })
|
||||
.toMatchObject({ ok: true, value: { accepted: true } })
|
||||
expect(followup).toHaveBeenCalledOnce()
|
||||
expect(agent.session.snapshotEvents()).toEqual(events)
|
||||
expect(currentSelection(ctx, sessionId)).toMatchObject({ provider: 'deepseek-official', model: 'removed-model' })
|
||||
await ctx.fiber.dispose()
|
||||
|
||||
@@ -522,4 +522,7 @@
|
||||
# key coming from the credential store below. Thinking defaults are a deployment
|
||||
# choice.
|
||||
- id: llm-deepseek
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
name: '@deepseek-ai/dsh-llm-deepseek-api-key'
|
||||
|
||||
- id: llm-deepseek-account
|
||||
name: '@deepseek-ai/dsh-llm-deepseek-account'
|
||||
@@ -59,7 +59,8 @@
|
||||
"@deepseek-ai/dsh-goal-round-driver": "workspace:*",
|
||||
"@deepseek-ai/dsh-api-gateway": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm-deepseek": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm-deepseek-api-key": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm-deepseek-account": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm-pi-ai": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm-retry": "workspace:*",
|
||||
"@deepseek-ai/dsh-mcp-resources": "workspace:*",
|
||||
|
||||
@@ -148,7 +148,6 @@ async function bench(script: Script, options: BenchOptions = {}): Promise<{
|
||||
options: createOptions.agentOptions ?? {},
|
||||
session,
|
||||
inbox,
|
||||
activeProvider: undefined,
|
||||
status: 'idle',
|
||||
ctx: ownerCtx,
|
||||
cancel: () => {},
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
name: '@deepseek-ai/dsh-plugin-package-inventory-deepseek'
|
||||
|
||||
- id: llm-deepseek
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
name: '@deepseek-ai/dsh-llm-deepseek-api-key'
|
||||
config:
|
||||
apiKeyEnv: DEEPSEEK_API_KEY
|
||||
defaultContextWindow: !!js Number(process.env.DSH_CONTEXT_WINDOW ?? 1000000)
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"@deepseek-ai/dsh-invariants": "workspace:*",
|
||||
"@deepseek-ai/dsh-jobs-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm-deepseek": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm-deepseek-api-key": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm-retry": "workspace:*",
|
||||
"@deepseek-ai/dsh-mcp-resources": "workspace:*",
|
||||
"@deepseek-ai/dsh-plugin-package-inventory-deepseek": "workspace:*",
|
||||
|
||||
@@ -31,7 +31,7 @@ describe('dsh-sdk-minimal bundle', () => {
|
||||
['deepseek-llm-api-extensions', '@deepseek-ai/dsh-deepseek-llm-api-extensions'],
|
||||
['session-log-deepseek', '@deepseek-ai/dsh-session-log-deepseek'],
|
||||
['plugin-package-inventory-deepseek', '@deepseek-ai/dsh-plugin-package-inventory-deepseek'],
|
||||
['llm-deepseek', '@deepseek-ai/dsh-llm-deepseek'],
|
||||
['llm-deepseek', '@deepseek-ai/dsh-llm-deepseek-api-key'],
|
||||
['sandbox', '@deepseek-ai/dsh-sandbox-local'],
|
||||
['session-projection', '@deepseek-ai/dsh-session-projection'],
|
||||
['sandbox-policy', '@deepseek-ai/dsh-sandbox-policy'],
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/ui-conversation/README.md
|
||||
README.md: 3c18d65e996812f08a6c70a74669e8449737968e
|
||||
README.zh.md: b48d2cf38aba13d42bff524004b077ff0dc179f0
|
||||
README.md: 4823aa0a1ba5975c4efc8b6bdddd7ca8c57c068f
|
||||
README.zh.md: 5ef8a1b6621bf8bab7e98cb18722a9aedda29101
|
||||
@@ -68,7 +68,7 @@ Active transcripts in the main occurrence expose content-width drag handles in t
|
||||
|
||||
The width-handle indicator follows the pointer only during a captured drag; ordinary hover leaves its position unchanged.
|
||||
|
||||
The resident composer survives no-Session and Session transitions. Whitespace hides its placeholder; a whitespace-only draft without attachments cannot be sent. The no-Session state keeps the same composer surface mounted but inert while the Workspace picker connects a blank Session. The surface is a shell-owned Lexical editor: reference chips are atomic decorator nodes carrying the owner's serialization identity (submission expands them through the owner codec), claimed slash commands stay styled leading text, folder text references carry the folder glyph as an icon prefix, and the draft's clipboard projection is mirrored into the per-Session Conversation store. QueueDock reads `next-turn` from the Session `inbox` projection, including cold recovered messages, and excludes only requestIds still owned by local transcript submissions. Other queued rows retain their normal display and actions. Queue operations address exact queue occurrences through the scoped `ctx.conversation` service; queue previews render sent text through the shared inline reference projection from `ui-primitives` (wire session forms fold to their label) and show local or durable images and files in original attachment order. Images use thumbnails; files use compact name-and-size cards. An edit exposes the literal sent text in a textarea that grows with its content, so line breaks survive re-editing; Enter saves, Shift+Enter breaks the line, and Escape cancels. Durable thumbnails resolve through the session image URL cache. Busy Enter behavior is stored in the Host-backed `ui-conversation` settings namespace. The composer keymap arbitrates the trigger menu's keys through the slash pipeline — Tab settles the highlighted completion (or drills a drillable one), Escape and Shift+Tab leave the menu without settling — and leaves every other key to the editor. An overlay that takes the keyboard hands it back through `SessionInput.focus()`, which rides Lexical's own focus so the caret returns where the draft left it rather than at the start. Model availability blocks retain the normal input placeholder while leaving the model picker enabled.
|
||||
The resident composer survives no-Session and Session transitions. Whitespace hides its placeholder; a whitespace-only draft without attachments cannot be sent. The no-Session state keeps the same composer surface mounted but inert while the Workspace picker connects a blank Session. The surface is a shell-owned Lexical editor: reference chips are atomic decorator nodes carrying the owner's serialization identity (submission expands them through the owner codec), claimed slash commands stay styled leading text, folder text references carry the folder glyph as an icon prefix, and the draft's clipboard projection is mirrored into the per-Session Conversation store. QueueDock reads `next-turn` from the Session `inbox` projection, including cold recovered messages, and excludes only requestIds still owned by local transcript submissions. Other queued rows retain their normal display and actions. Queue operations address exact queue occurrences through the scoped `ctx.conversation` service; queue previews render sent text through the shared inline reference projection from `ui-primitives` (wire session forms fold to their label) and show local or durable images and files in original attachment order. Images use thumbnails; files use compact name-and-size cards. An edit exposes the literal sent text in a textarea that grows with its content, so line breaks survive re-editing; Enter saves, Shift+Enter breaks the line, and Escape cancels. Durable thumbnails resolve through the session image URL cache. Busy Enter behavior is stored in the Host-backed `ui-conversation` settings namespace. The composer keymap arbitrates the trigger menu's keys through the slash pipeline — Tab settles the highlighted completion (or drills a drillable one), Escape and Shift+Tab leave the menu without settling — and leaves every other key to the editor. An overlay that takes the keyboard hands it back through `SessionInput.focus()`, which rides Lexical's own focus so the caret returns where the draft left it rather than at the start.
|
||||
|
||||
Default sends commit optimistically: Enter clears the draft, occurrence table, and undo history in the same transaction, keeps the composer in `plain`, and runs the send as a detached attempt, so typing and further sends continue during the flight. `sendSession` registers a Session submission echo (`session.beginSubmission`) with the delivery mode before serializing, preserving selected image and file order in `pendingSubmissions`; Session derives the placement from that mode and its current running state, so idle sends use the transcript, busy Queue sends use QueueDock, and busy Steer sends use the pending-steering surface. It then yields one paint, encodes images through the browser's native `FileReader` data-URL path, and cites staged file receipts. Command submissions use the same receipts for generic files, so sending `/goal` or `/plan` never reads those browser files again. The prompt reuses the submission `requestId`; Session retires the echo once at its display handoff, correlated by that `rpcId`. Concurrent failures are restored together in submission order until the user edits the restored content; command submissions keep the frozen `submitting` phase. Detached attempts retain their attachment ids through admission and Session scope disposal. An observed retirement immediately exposes each image preview through the durable cache, replaces it with the canonical URL after fetching the admitted attachment, revokes each URL after its use ends, and releases file cards. Selected generic files enter one FIFO background-upload queue; `maxConcurrentFileUploads` defaults to two active Worker transports, the Conversation service retains queued and active operations plus byte progress across Session navigation, and removing a draft skips its queued transfer or aborts its active transport. Where the browser shell exposes `__DSH_HOST_PATHS__` (the Desktop application), dropped or pasted folders and dropped, picked, or pasted non-image files with a real path become `@path` chips; images keep uploading. Drag-and-drop and paste identify directories through the browser entry API; when that API is absent or returns no entry, pasted items retain ordinary file handling. The file picker cannot select directories. References require the `ui-reference` plugin and the original paths to remain readable by the model's file tools. Paths inside the workspace are relative; others remain absolute. The whole batch is validated before insertion, retains source order and selected text, and uses whitespace-separated, closed-quote mentions. A browser without the bridge refuses dropped or pasted folders; a Desktop folder without a reported path is rejected separately. Continuable subagents disable attachment intake and skip local echoes because their transport does not preserve the browser request id.
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ Session 首次绑定或缓存的 Session 成为 current 时,shell 会在渲染
|
||||
|
||||
宽度拖拽条的指示线只在已捕获指针的拖拽期间跟随指针,普通悬停不改变其位置。
|
||||
|
||||
常驻 composer 在无 Session 与有 Session 之间保持挂载。输入空白字符会隐藏占位提示;没有附件的纯空白草稿无法发送。无 Session 时,同一个编辑器表面保持 inert,Workspace picker 连接 blank Session。该表面是 shell 所有的 Lexical 编辑器:引用 chip 是携带 owner 序列化身份的原子 decorator 节点(提交时经 owner codec 展开),已认领的 slash command 保持为带样式的行首文本,文件夹文本引用以图标前缀携带文件夹图形,草稿的剪贴板投影镜像到逐 Session Conversation store。QueueDock 从 Session 的 `inbox` 投影读取 `next-turn`,包含从冷状态恢复的消息,仅排除仍由本地 transcript 提交承接的 requestId。其他排队行保留正常展示和操作。Queue 操作通过 scoped `ctx.conversation` service 寻址准确的 queue occurrence;queue 预览经 `ui-primitives` 的共享行内引用投影渲染已发送文本(wire 会话形式折叠为其标签),并按原始附件顺序展示本地或持久化的图片和文件。图片使用缩略图,文件使用紧凑的名称与大小卡片。编辑态在可随内容增高的 textarea 中展示字面发送文本,因此重新编辑不会丢失换行;Enter 保存,Shift+Enter 换行,Escape 取消。持久化缩略图通过会话图片 URL 缓存解析。繁忙时 Enter 行为保存在 Host-backed `ui-conversation` settings namespace。 composer 键盘映射经斜杠流水线裁决触发菜单的按键——Tab 确认高亮补全项(可下钻项则下钻),Escape 与 Shift+Tab 离开菜单且不选定——其余按键交给编辑器自身。 接管键盘的浮层通过 `SessionInput.focus()` 把键盘还回来,该路径走 Lexical 自己的 focus,因此光标回到草稿原来的位置而不是开头。 模型不可用导致的阻塞保留常规输入占位文案,模型选择器仍可操作。
|
||||
常驻 composer 在无 Session 与有 Session 之间保持挂载。输入空白字符会隐藏占位提示;没有附件的纯空白草稿无法发送。无 Session 时,同一个编辑器表面保持 inert,Workspace picker 连接 blank Session。该表面是 shell 所有的 Lexical 编辑器:引用 chip 是携带 owner 序列化身份的原子 decorator 节点(提交时经 owner codec 展开),已认领的 slash command 保持为带样式的行首文本,文件夹文本引用以图标前缀携带文件夹图形,草稿的剪贴板投影镜像到逐 Session Conversation store。QueueDock 从 Session 的 `inbox` 投影读取 `next-turn`,包含从冷状态恢复的消息,仅排除仍由本地 transcript 提交承接的 requestId。其他排队行保留正常展示和操作。Queue 操作通过 scoped `ctx.conversation` service 寻址准确的 queue occurrence;queue 预览经 `ui-primitives` 的共享行内引用投影渲染已发送文本(wire 会话形式折叠为其标签),并按原始附件顺序展示本地或持久化的图片和文件。图片使用缩略图,文件使用紧凑的名称与大小卡片。编辑态在可随内容增高的 textarea 中展示字面发送文本,因此重新编辑不会丢失换行;Enter 保存,Shift+Enter 换行,Escape 取消。持久化缩略图通过会话图片 URL 缓存解析。繁忙时 Enter 行为保存在 Host-backed `ui-conversation` settings namespace。 composer 键盘映射经斜杠流水线裁决触发菜单的按键——Tab 确认高亮补全项(可下钻项则下钻),Escape 与 Shift+Tab 离开菜单且不选定——其余按键交给编辑器自身。 接管键盘的浮层通过 `SessionInput.focus()` 把键盘还回来,该路径走 Lexical 自己的 focus,因此光标回到草稿原来的位置而不是开头。
|
||||
|
||||
默认发送采用乐观提交:Enter 在同一事务里清空草稿、occurrence 表和撤销历史,composer 保持 `plain`,发送作为 detached attempt 运行,发送期间可以继续输入和提交。`sendSession` 在序列化之前用投递模式注册 Session 提交回显(`session.beginSubmission`),并在 `pendingSubmissions` 中保留图片与文件的选择顺序;Session 根据该模式与当前运行状态推导位置,因此空闲发送进入 transcript(文本记录),繁忙时 Queue 进入 QueueDock,繁忙时 Steer 进入 pending-steering 区域。随后让出一帧,图片经浏览器原生 `FileReader` data-URL 路径编码,文件则引用已暂存凭证。命令提交也用同一凭证表示通用文件,因此发送 `/goal` 或 `/plan` 时不会再次读取这些浏览器文件。提示词复用提交 `requestId`;Session 按同一 `rpcId` 关联展示接管,并仅退休回显一次。多个并发发送失败时,在用户编辑还原内容之前按提交顺序合并还原;命令提交保持冻结的 `submitting` 阶段。Detached attempt 持有附件 id,直到 admission 完成或 Session scope 销毁。回显以 observed 退休时,durable 图片缓存立即公开每个预览 URL,读取 admitted 附件后用规范化 URL 替换预览,并在各 URL 停止使用后撤销,同时释放文件卡。选中的通用文件进入同一个先进先出的后台上传队列;`maxConcurrentFileUploads` 默认允许两个 Worker transport 同时运行,Conversation 服务在切换 Session 时继续持有排队和运行中的传输操作及字节进度,移除草稿会跳过排队中的传输或中止正在运行的传输。浏览器 shell 暴露 `__DSH_HOST_PATHS__` 时(桌面应用),拖入或粘贴的文件夹以及拖入、选择或粘贴的带真实路径的非图片文件会成为 `@路径` chip;图片仍然上传。拖放和粘贴通过浏览器 entry API 识别目录;该 API 不可用或没有返回 entry 时,粘贴项沿用普通文件处理。文件选择器不能选择目录。引用需要启用 `ui-reference` 插件,原路径也必须仍可由模型的文件工具读取。工作区内的路径使用相对形式,其他路径保留绝对形式。整批文件先校验再插入,保留来源顺序和已选中的文字,引用之间有空白分隔,含空格的路径使用闭合引号。没有该桥的浏览器会拒绝拖入或粘贴的文件夹,桌面端无法获取文件夹路径时单独报错。continuable 子代理禁用附件入口,也不创建本地回显,因为其 transport 不保留浏览器 request id。
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ export function ConversationContent(props: ConversationContentProps) {
|
||||
// bar is ONE session-maybe slot rendered unconditionally — inert is a prop,
|
||||
// not a different tree, so the textarea DOM survives the transition.
|
||||
const inert = sessionId === undefined || (hero && chipTitle === undefined)
|
||||
// A raised block keeps the same textarea mounted:
|
||||
// A raised block is the same inert posture with the blocker's own reason:
|
||||
// one disabled textarea, never a second tree. The no-workspace state wins
|
||||
// when both hold — picking a workspace is the earlier prerequisite.
|
||||
const blocked = !inert && composerBlock !== undefined
|
||||
@@ -150,7 +150,7 @@ export function ConversationContent(props: ConversationContentProps) {
|
||||
// `blocked`, not `disabled`: the bar refuses input either way, but a
|
||||
// block keeps the model seat live because choosing a model is how the
|
||||
// user clears it.
|
||||
? { blocked: composerBlock, placeholder: t(hero ? 'placeholder.hero' : 'placeholder.default') }
|
||||
? { blocked: composerBlock, placeholder: composerBlock.reason }
|
||||
: hero ? { placeholder: t('placeholder.hero') } : {}),
|
||||
})
|
||||
|
||||
|
||||
@@ -425,14 +425,15 @@ describe('ConversationRoot resident composer', () => {
|
||||
expect(dispatchCount()).toBe(before)
|
||||
})
|
||||
|
||||
it('keeps the normal placeholder while model availability blocks sending', () => {
|
||||
it('renders the composer inert with the blocker\u2019s own reason', () => {
|
||||
const b = mount(sessionSnapshotOf(), undefined, undefined, {
|
||||
composerBlock: { reason: 'select a model first' },
|
||||
})
|
||||
const box = b.view.getByRole('textbox')
|
||||
// Blocking keeps the mounted composer and its normal placeholder.
|
||||
// One disabled composer with the blocker's placeholder, never a second
|
||||
// tree: the DOM survives the block being raised and cleared.
|
||||
expect(box.getAttribute('aria-disabled')).toBe('true')
|
||||
expect(box.getAttribute('data-placeholder')).toBe(zh['placeholder.default'])
|
||||
expect(box.getAttribute('data-placeholder')).toBe('select a model first')
|
||||
fireEvent.keyDown(box, { key: 'Enter' })
|
||||
expect(b.sink).not.toHaveBeenCalled()
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/ui-model-selection/README.md
|
||||
README.md: aa4a9150a81417241227f99ec747a617640f1302
|
||||
README.zh.md: 79516f159d95335561b95bbc8f8be1ecd8ccf9b0
|
||||
README.md: b741e05a983fd35ab9d17164b020ff5ba86137b7
|
||||
README.zh.md: 1ce5a2b1ae191ac2da86f6a4ac854bd3c9431c8c
|
||||
@@ -29,7 +29,7 @@ DeepSeek account and API-key routes appear as separate provider groups, each exp
|
||||
|
||||
The unselected model label uses the same regular weight as an available model name and retains the saved reasoning effort caption for existing and new sessions; effort editing requires an available model. Clicking the unselected trigger opens the model list directly; Escape closes it.
|
||||
|
||||
Mount this plugin alongside `ui-conversation` and the commands package; the composer then shows the model seat next to the pending indicator, and `/model` opens the same directory as a popup. While the seat's menu is open, `↑`/`↓` move focus across the rows of the shown pane, Tab settles the focused row, and Escape and `Shift+Tab` leave a drilled pane first and otherwise close back to the trigger. Drilling lands on the row of the value in use, and going back lands on the cell that opened the pane left. Both surfaces show the host-reported current selection when the exact provider/model pair remains in the advertised groups; a missing catalog row clears the effective selection and the trigger prompts `Select model`, without changing the stored choice.
|
||||
Mount this plugin alongside `ui-conversation` and the commands package; the composer then shows the model seat next to the pending indicator, and `/model` opens the same directory as a popup. While the seat's menu is open, `↑`/`↓` move focus across the rows of the shown pane, Tab settles the focused row, and Escape and `Shift+Tab` leave a drilled pane first and otherwise close back to the trigger. Drilling lands on the row of the value in use, and going back lands on the cell that opened the pane left. The composer shows the catalog name while the selected model is available, and its saved `provider/model` ID when the model or provider is removed, including account sign-out. The stored provider, model, and reasoning effort remain unchanged.
|
||||
|
||||
### Model and effort
|
||||
|
||||
@@ -39,9 +39,7 @@ The composer replaces the model and effort text with the Models icon when the ex
|
||||
|
||||
### Unroutable sessions
|
||||
|
||||
The composer blocks sending while the exact provider/model pair is absent from the available catalog or availability is loading or unknown. It keeps the normal input placeholder and an enabled model picker. Refreshes and refresh failures retain the last displayed selection and groups while sending remains blocked; retry revalidates availability. A Host reset clears that display. The account provider name follows the active locale in both selectors. Sign-out, credential removal, and provider or model removal refresh availability without replacing the saved choice with another model. Existing session logs remain unchanged.
|
||||
|
||||
Only the current Client binding's directory can publish its composer block. Cleanup from an older binding preserves a replacement directory's block; without a replacement directory, cleanup removes the obsolete block.
|
||||
Catalog availability does not block sending with a saved selection; request execution reports missing credentials or unavailable models. Refreshes and refresh failures retain the last displayed selection and groups. A Host reset clears that display. Sign-out hides the account provider from the picker while preserving the saved provider/model ID and reasoning effort. Signing in restores the catalog name when that model is available again. Existing session logs remain unchanged.
|
||||
|
||||
### Selection failures
|
||||
|
||||
@@ -67,7 +65,7 @@ Two entries over ONE per-session directory owned by `ModelDirectoryResolver` (`c
|
||||
Read these pages when the model surface is not enough. They move from the browser surfaces to the command popup shell and the selection contract.
|
||||
|
||||
- [ui-commands](../ui-commands/README.md) — the popupSelect shell the `/model` contribution registers into.
|
||||
- [ui-conversation](../ui-conversation/README.md) — declares the composer's `conversation.input.model` seat and the composer block.
|
||||
- [ui-conversation](../ui-conversation/README.md) — declares the composer's `conversation.input.model` seat.
|
||||
- [dsh-agent-default-model](../../core/agent-default-model/README.md) — the default-model service for sessions that never choose.
|
||||
- [Client package map](../README.md) — adjacent browser UI packages.
|
||||
|
||||
@@ -86,7 +84,6 @@ Switching the route can reduce or invalidate provider-side cache reuse for subse
|
||||
|
||||
<a id="known-limitations-and-deferred-work"></a>
|
||||
|
||||
|
||||
These limits define the current model surface. They are current package constraints, not a general model-router comparison or a task backlog.
|
||||
|
||||
- **No create-time or addressed-subagent selection** — both entries require an existing ordinary session's Agent; there is no draft-phase model choice to fold into session creation, and subagent continuation deliberately exposes no independent model-selection contract.
|
||||
|
||||
@@ -29,7 +29,7 @@ DeepSeek 账号和 API Key 路由显示为独立提供方分组,各自展示
|
||||
|
||||
未选模型文案与可用模型名使用相同的常规字重,并在既有会话与新会话中保留已保存的推理强度显示;调整推理强度需要先选择可用模型。点击未选模型控件直接打开模型列表,Escape 关闭列表。
|
||||
|
||||
与 `ui-conversation` 及命令包一起挂载本插件;composer 随即在待处理指示器旁显示模型位,`/model` 则以弹窗打开同一份目录。模型位菜单打开期间,`↑`/`↓` 在所显示面板的行间移动焦点,`Tab` 选定聚焦行,Escape 与 `Shift+Tab` 先退出已下钻的面板,否则关闭并回到触发器。下钻落在正在使用的那一行,返回则落在打开该面板的格子上。当确切提供方/模型对仍在已公布分组中时,两个界面都显示 Host 报告的当前选择;目录行缺席时,有效选择置空,触发器提示“请选择模型”,已保存的选择不变。
|
||||
与 `ui-conversation` 及命令包一起挂载本插件;composer 随即在待处理指示器旁显示模型位,`/model` 则以弹窗打开同一份目录。模型位菜单打开期间,`↑`/`↓` 在所显示面板的行间移动焦点,`Tab` 选定聚焦行,Escape 与 `Shift+Tab` 先退出已下钻的面板,否则关闭并回到触发器。下钻落在正在使用的那一行,返回则落在打开该面板的格子上。所选模型可用时,composer 显示目录中的模型名称;模型或提供方被删除时(包括账号退登),改为显示已保存的 `provider/model` ID。保存的提供方、模型和推理强度均保持不变。
|
||||
|
||||
### 模型与推理强度
|
||||
|
||||
@@ -39,9 +39,7 @@ DeepSeek 账号和 API Key 路由显示为独立提供方分组,各自展示
|
||||
|
||||
### 不可路由的会话
|
||||
|
||||
确切提供方/模型对不在可用目录中,或可用性仍在加载或未知时,composer 禁止发送。输入框保留常规占位文案,模型选择器仍可操作。刷新及刷新失败期间保留上次显示的选择和分组,同时继续禁发;重试会重新确认可用性。Host 重置时清空这些显示。两个选择器中的账号提供方名称均随当前语言显示。退出账号、移除凭据、提供方或模型都会刷新可用性,不会用其他模型替换已保存的选择。既有会话日志保持不变。
|
||||
|
||||
只有当前 Client binding 的目录可以发布对应的 composer 阻塞块。旧 binding 的清理保留后继目录的阻塞块;若尚无后继目录,则移除已失效的阻塞块。
|
||||
目录可用性不会阻止使用已保存选择发送消息;请求执行负责报告凭据缺失或模型不可用。刷新及刷新失败期间保留上次显示的选择和分组。Host 重置时清空这些显示。退登后选择器隐藏账号提供方,同时保留已保存的提供方/模型 ID 和推理强度。再次登录且该模型可用时恢复目录名称。既有会话日志保持不变。
|
||||
|
||||
### 选择失败
|
||||
|
||||
@@ -67,7 +65,7 @@ DeepSeek 账号和 API Key 路由显示为独立提供方分组,各自展示
|
||||
当仅了解模型界面还不够时,请阅读以下页面。这些页面从浏览器界面逐步深入到命令弹窗外壳与选择约定。
|
||||
|
||||
- [ui-commands](../ui-commands/README.zh.md)——`/model` 贡献项注册进的 popupSelect 外壳。
|
||||
- [ui-conversation](../ui-conversation/README.zh.md)——声明 composer 的 `conversation.input.model` 位与 composer 阻塞块。
|
||||
- [ui-conversation](../ui-conversation/README.zh.md)——声明 composer 的 `conversation.input.model` 位。
|
||||
- [dsh-agent-default-model](../../core/agent-default-model/README.zh.md)——为从未选择的会话提供默认模型的默认模型服务。
|
||||
- [客户端包映射](../README.zh.md)——相邻的浏览器 UI 包。
|
||||
|
||||
@@ -86,7 +84,6 @@ DeepSeek 账号和 API Key 路由显示为独立提供方分组,各自展示
|
||||
|
||||
<a id="known-limitations-and-deferred-work"></a>
|
||||
|
||||
|
||||
这些限制界定了当前模型选择界面。它们是当前包约束,不是通用模型路由器对比或任务积压。
|
||||
|
||||
- **无创建期或已寻址 subagent 选择**——两个入口都要求既有普通会话的 agent(智能体);没有可纳入会话创建的草稿阶段模型选择,subagent 继续执行也有意不公开独立的模型选择约定。
|
||||
|
||||
@@ -15,7 +15,7 @@ import type { ModelCatalogDirectory } from './catalog.ts'
|
||||
|
||||
/** Directory snapshot both entries render from. */
|
||||
export interface ModelDirectoryState {
|
||||
/** Last catalog-confirmed selection, retained for display during same-Host refreshes. */
|
||||
/** Saved selection, retained even when its provider or model leaves the catalog. */
|
||||
current: ModelSelection | null
|
||||
/** Saved effort caption retained when the selected model is unavailable. */
|
||||
retainedEffort?: string
|
||||
@@ -156,12 +156,12 @@ export class ModelDirectory {
|
||||
return
|
||||
}
|
||||
const selection = projected.next ?? catalog.value.default
|
||||
const current = catalog.value.groups.some(group => group.id === selection.provider
|
||||
&& group.models.some(model => model.id === selection.model)) ? selection : null
|
||||
const routable = catalog.value.groups.some(group => group.id === selection.provider
|
||||
&& group.models.some(model => model.id === selection.model))
|
||||
this.store.set({
|
||||
current,
|
||||
current: selection,
|
||||
...retainedEffort === undefined ? {} : { retainedEffort },
|
||||
routable: current !== null,
|
||||
routable,
|
||||
groups: catalog.value.groups,
|
||||
failures: catalog.value.failures,
|
||||
status: this.store.getSnapshot().status === 'selecting'
|
||||
|
||||
@@ -135,9 +135,7 @@ export function apply(ctx: ClientContext): void {
|
||||
// through the bound translate; the seat component reads the standard seat.
|
||||
const t = ctx.locale.bind(NS)
|
||||
|
||||
// The composer-block reason is this plugin's own copy, read at raise time so
|
||||
// a locale change reaches the next publish.
|
||||
ctx.plugin(ModelDirectoryResolver, { blockReason: () => t('blocked.composer') })
|
||||
ctx.plugin(ModelDirectoryResolver)
|
||||
|
||||
// Entry 1: the /model popupSelect over the shared directory.
|
||||
ctx.inject(['commandUi', 'modelDirectories'], (scope: ClientContext) => {
|
||||
|
||||
@@ -31,7 +31,6 @@ export const zh = {
|
||||
'action.reload': '重新加载',
|
||||
'warning.groupLoad': '{name} 加载失败:{message}',
|
||||
'empty.models': '没有可用的模型。',
|
||||
'blocked.composer': '当前模型不可用,请先选择模型',
|
||||
'empty.efforts': '当前模型未提供推理等级。',
|
||||
} satisfies Record<string, string>
|
||||
|
||||
@@ -61,6 +60,5 @@ export const en = {
|
||||
'action.reload': 'Reload',
|
||||
'warning.groupLoad': '{name} failed to load: {message}',
|
||||
'empty.models': 'No models available.',
|
||||
'blocked.composer': 'This model is unavailable — select one to continue',
|
||||
'empty.efforts': 'This model provides no reasoning effort levels.',
|
||||
} satisfies Record<ModelKey, string>
|
||||
@@ -39,16 +39,11 @@ export class ModelDirectoryResolver extends Service {
|
||||
private readonly live: LiveState = { directories: new WeakMapWithValues() }
|
||||
private readonly catalog: ModelCatalogDirectory
|
||||
|
||||
/** Localized composer-block copy; this plugin owns the string it raises. */
|
||||
private readonly blockReason: () => string
|
||||
|
||||
/**
|
||||
* @param ctx - owning root context (the service registers itself as `models`).
|
||||
* @param config - the bound translator for this plugin's own dictionary.
|
||||
*/
|
||||
constructor(ctx: Context, config: { blockReason: () => string }) {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'modelDirectories')
|
||||
this.blockReason = config.blockReason
|
||||
this.catalog = new ModelCatalogDirectory(ctx)
|
||||
void this.catalog.load().catch(() => { /* selectors expose the shared error */ })
|
||||
ctx.on('connection/reset', () => {
|
||||
@@ -84,25 +79,6 @@ export class ModelDirectoryResolver extends Service {
|
||||
binding.session.projections.faceOf('modelSelection'),
|
||||
)
|
||||
live.directories.set(binding, directory)
|
||||
const conversation = this.ctx.get('conversation')
|
||||
if (conversation !== undefined) {
|
||||
const publish = (): void => {
|
||||
if (sessions.binding(sessionId) !== binding) return
|
||||
conversation.blocks.set(sessionId, directory.store.getSnapshot().routable !== true
|
||||
? { reason: this.blockReason() }
|
||||
: undefined)
|
||||
}
|
||||
publish()
|
||||
actx.effect(() => {
|
||||
const stop = directory.store.subscribe(publish)
|
||||
return () => {
|
||||
stop()
|
||||
const current = sessions.binding(sessionId)
|
||||
if (current !== undefined && current !== binding && live.directories.get(current) !== undefined) return
|
||||
conversation.blocks.set(sessionId, undefined)
|
||||
}
|
||||
}, 'ui-model-selection: composer block')
|
||||
}
|
||||
actx.effect(() => () => {
|
||||
directory.dispose()
|
||||
live.directories.delete(binding)
|
||||
|
||||
@@ -356,7 +356,7 @@ describe('ui-model-selection dual entry', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('blocks until a refreshed catalog validates the projected selection', async () => {
|
||||
it('retains the selected model while refreshing its catalog', async () => {
|
||||
const b = await bench()
|
||||
b.mint('s1')
|
||||
const face = b.seat().inject!(sid('s1'))
|
||||
@@ -383,7 +383,7 @@ describe('ui-model-selection dual entry', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('retains the last catalog on refresh failure, blocks sending, and recovers on retry', async () => {
|
||||
it('retains the last catalog on refresh failure and recovers on retry', async () => {
|
||||
const b = await bench()
|
||||
try {
|
||||
b.mint('s1')
|
||||
@@ -397,7 +397,7 @@ describe('ui-model-selection dual entry', () => {
|
||||
groups: GROUPS, routable: null, status: 'error', error: 'catalog offline',
|
||||
})
|
||||
})
|
||||
expect(b.blockOf('s1')).toBeDefined()
|
||||
expect(b.blockOf('s1')).toBeUndefined()
|
||||
b.setCatalogFailure(false)
|
||||
await b.ctx.modelDirectories.directoryFor(sid('s1')).load()
|
||||
expect(face.directory.getSnapshot().routable).toBe(true)
|
||||
@@ -417,68 +417,6 @@ describe('ui-model-selection dual entry', () => {
|
||||
expect(face2.directory).not.toBe(face1.directory)
|
||||
})
|
||||
|
||||
it('keeps the replacement directory block when the previous scope finishes cleanup', async () => {
|
||||
const b = await bench()
|
||||
const first = b.mint('s1')
|
||||
const oldDirectory = b.ctx.modelDirectories.directoryFor(sid('s1'))
|
||||
await oldDirectory.load()
|
||||
const replacement = b.mint('s1')
|
||||
replacement.projection.set({ lastUsed: null, next: { provider: 'missing', model: 'unserved' } })
|
||||
const directory = b.ctx.modelDirectories.directoryFor(sid('s1'))
|
||||
try {
|
||||
expect(directory).not.toBe(oldDirectory)
|
||||
expect(directory.store.getSnapshot().routable).toBe(false)
|
||||
expect(b.blockOf('s1')?.reason).toBe(zh['blocked.composer'])
|
||||
await first.fiber.dispose()
|
||||
expect(b.blockOf('s1')?.reason).toBe(zh['blocked.composer'])
|
||||
await replacement.fiber.dispose()
|
||||
expect(b.blockOf('s1')).toBeUndefined()
|
||||
} finally {
|
||||
await Promise.all([first.fiber.dispose(), replacement.fiber.dispose()])
|
||||
await b.ctx.fiber.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it.each([false, true])('ignores late old projection publication while the replacement is routable=%s', async (routable) => {
|
||||
const b = await bench()
|
||||
const first = b.mint('s1')
|
||||
const oldDirectory = b.ctx.modelDirectories.directoryFor(sid('s1'))
|
||||
await oldDirectory.load()
|
||||
const replacement = b.mint('s1')
|
||||
replacement.projection.set({ lastUsed: null,
|
||||
next: { provider: routable ? 'deepseek-official' : 'missing', model: 'deepseek-v4-flash' } })
|
||||
const directory = b.ctx.modelDirectories.directoryFor(sid('s1'))
|
||||
try {
|
||||
expect(directory.store.getSnapshot().routable).toBe(routable)
|
||||
const expected = b.blockOf('s1')
|
||||
await Promise.resolve().then(() => {
|
||||
first.projection.set({ lastUsed: null,
|
||||
next: { provider: routable ? 'missing' : 'deepseek-official', model: 'deepseek-v4-pro' } })
|
||||
})
|
||||
expect(oldDirectory.store.getSnapshot().routable).toBe(!routable)
|
||||
expect(b.blockOf('s1')).toBe(expected)
|
||||
} finally {
|
||||
await Promise.all([first.fiber.dispose(), replacement.fiber.dispose()])
|
||||
await b.ctx.fiber.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it('clears an obsolete block when the new binding has no model directory', async () => {
|
||||
const b = await bench()
|
||||
const first = b.mint('s1')
|
||||
first.projection.set({ lastUsed: null, next: { provider: 'missing', model: 'unserved' } })
|
||||
await b.ctx.modelDirectories.directoryFor(sid('s1')).load()
|
||||
const replacement = b.mint('s1')
|
||||
try {
|
||||
expect(b.blockOf('s1')).toBeDefined()
|
||||
await first.fiber.dispose()
|
||||
expect(b.blockOf('s1')).toBeUndefined()
|
||||
} finally {
|
||||
await Promise.all([first.fiber.dispose(), replacement.fiber.dispose()])
|
||||
await b.ctx.fiber.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it('retains saved effort for existing and new sessions after credentials disappear', async () => {
|
||||
const b = await bench()
|
||||
try {
|
||||
@@ -492,17 +430,17 @@ describe('ui-model-selection dual entry', () => {
|
||||
b.remote.emit('settings/document-updated', ['llm-deepseek', 1])
|
||||
expect(existing.store.getSnapshot().retainedEffort).toBe('High')
|
||||
await vi.waitFor(() => {
|
||||
expect(existing.store.getSnapshot()).toMatchObject({ current: null, routable: false, retainedEffort: 'High' })
|
||||
expect(existing.store.getSnapshot()).toMatchObject({ current: { provider: 'deepseek-official', model: 'deepseek-v4-flash', reasoningEffort: 'high' }, routable: false, retainedEffort: 'High' })
|
||||
})
|
||||
b.mint('new')
|
||||
const fresh = b.ctx.modelDirectories.directoryFor(sid('new'))
|
||||
expect(fresh.store.getSnapshot()).toMatchObject({ current: null, routable: false, retainedEffort: 'Max' })
|
||||
expect(fresh.store.getSnapshot()).toMatchObject({ current: { provider: 'deepseek-official', model: 'deepseek-v4-flash', reasoningEffort: 'max' }, routable: false, retainedEffort: 'Max' })
|
||||
} finally {
|
||||
await b.ctx.fiber.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it('blocks the composer when current catalog models disappear', async () => {
|
||||
it('keeps the composer usable when current catalog models disappear', async () => {
|
||||
const b = await bench()
|
||||
b.mint('s1')
|
||||
const face = b.seat().inject!(sid('s1'))
|
||||
@@ -518,7 +456,7 @@ describe('ui-model-selection dual entry', () => {
|
||||
b.remote.emit('settings/document-updated', ['llm-deepseek', 1])
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
expect(b.blockOf('s1')?.reason).toBe(zh['blocked.composer'])
|
||||
expect(b.blockOf('s1')).toBeUndefined()
|
||||
expect(b.calls.models).toBe(2)
|
||||
|
||||
// Recovering clears it without a reload of the surface.
|
||||
@@ -530,29 +468,16 @@ describe('ui-model-selection dual entry', () => {
|
||||
expect(b.calls.models).toBe(3)
|
||||
})
|
||||
|
||||
it('hides an unavailable durable selection without replacing or rewriting it', async () => {
|
||||
it('retains an unavailable durable selection without replacing or rewriting it', async () => {
|
||||
const b = await bench()
|
||||
b.mint('s1')
|
||||
const face = b.seat().inject!(sid('s1'))
|
||||
const intended = { provider: 'deepseek-official', model: 'unlisted' }
|
||||
b.setProjected(sid('s1'), { lastUsed: intended, next: intended })
|
||||
await vi.waitFor(() => { expect(face.directory.getSnapshot().status).toBe('ready') })
|
||||
expect(face.directory.getSnapshot().current).toBeNull()
|
||||
expect(b.blockOf('s1')?.reason).toBe(zh['blocked.composer'])
|
||||
expect(b.calls.select).toBe(0)
|
||||
})
|
||||
|
||||
it('clears its block when the session scope goes', async () => {
|
||||
const b = await bench()
|
||||
const scope = b.mint('s1')
|
||||
b.setRoutable(false)
|
||||
const face = b.seat().inject!(sid('s1'))
|
||||
face.load()
|
||||
b.remote.emit('llm/adapters-updated', [])
|
||||
await vi.waitFor(() => { expect(b.blockOf('s1')).toBeDefined() })
|
||||
|
||||
await scope.fiber.dispose()
|
||||
expect(face.directory.getSnapshot().current).toEqual(intended)
|
||||
expect(b.blockOf('s1')).toBeUndefined()
|
||||
expect(b.calls.select).toBe(0)
|
||||
})
|
||||
|
||||
it('an unknown session fails loud at the seat inject', async () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @vitest-environment jsdom
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { RemoteError } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
@@ -144,6 +144,19 @@ describe('ModelSelect reasoning effort', () => {
|
||||
expect(screen.queryByText('Fast catalog description')).toBeNull()
|
||||
})
|
||||
|
||||
it.each(['model', 'provider'])('keeps the saved id and effort when the selected %s disappears', (removed) => {
|
||||
const directory = createSnapshotStore(state({ retainedEffort: 'High' }))
|
||||
render(<ModelSelect locked={false} available directory={directory} load={vi.fn()} select={vi.fn()} t={t} />)
|
||||
expect(screen.getByRole('button', { name: /选择模型,当前/ }).textContent).toContain('DeepSeek-V4-Flash')
|
||||
act(() => { directory.update((snapshot) => {
|
||||
snapshot.groups = removed === 'provider' ? [] : snapshot.groups.map(group => ({ ...group, models: [] }))
|
||||
snapshot.routable = false
|
||||
}) })
|
||||
expect(screen.getByRole('button', { name: /选择模型,当前/ }).textContent)
|
||||
.toMatchInlineSnapshot('"deepseek-official/deepseek-v4-flashHigh"')
|
||||
expect(directory.getSnapshot().current).toEqual(state().current)
|
||||
})
|
||||
|
||||
it('shows loading until the catalog and Session projection are both ready', async () => {
|
||||
const directory = createSnapshotStore<ModelDirectoryState>(state({
|
||||
current: null,
|
||||
@@ -491,3 +504,19 @@ it.each([en, zh])('localizes the account group while preserving external names',
|
||||
expect(screen.getByRole('group', { name: copy['provider.account'] })).toBeTruthy()
|
||||
expect(screen.getByRole('group', { name: 'My Gateway' })).toBeTruthy()
|
||||
})
|
||||
|
||||
it('restores the account model name after login without changing the saved route', () => {
|
||||
const groups = [{ id: 'deepseek-account', name: 'DeepSeek Account', models: [
|
||||
{ id: 'deepseek-flash', name: 'DeepSeek Flash', reasoning },
|
||||
] }]
|
||||
const selected = { provider: 'deepseek-account', model: 'deepseek-flash', reasoningEffort: 'high' }
|
||||
const directory = createSnapshotStore(state({ current: selected, groups, retainedEffort: 'High' }))
|
||||
render(<ModelSelect locked={false} available directory={directory} load={vi.fn()} select={vi.fn()} t={t} />)
|
||||
expect(screen.getByRole('button', { name: /选择模型,当前/ }).textContent).toBe('DeepSeek FlashHigh')
|
||||
act(() => { directory.update((snapshot) => { snapshot.groups = []; snapshot.routable = false }) })
|
||||
expect(screen.getByRole('button', { name: /选择模型,当前/ }).textContent)
|
||||
.toMatchInlineSnapshot('"deepseek-account/deepseek-flashHigh"')
|
||||
act(() => { directory.update((snapshot) => { snapshot.groups = groups; snapshot.routable = true }) })
|
||||
expect(screen.getByRole('button', { name: /选择模型,当前/ }).textContent).toBe('DeepSeek FlashHigh')
|
||||
expect(directory.getSnapshot().current).toEqual(selected)
|
||||
})
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/ui-settings-account/README.md
|
||||
README.md: 55a747896f5c115d08ebacea6046b4de711a530c
|
||||
README.zh.md: 582a1b21113631305eb38bd02a140479704d365d
|
||||
README.md: 2a539ab2ed5836e8a9a2afaf4ff49f19e8d20fe8
|
||||
README.zh.md: b3d7cd1fb85ed445a32b796cd344c27b19d30229
|
||||
@@ -19,6 +19,8 @@ Desktop Account settings display DeepSeek login state and offer browser authoriz
|
||||
- [Model Experience](#model-experience)
|
||||
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
||||
|
||||
A new account-model sign-in-required event displays “Model unavailable. Please sign in and try again.” in a toast. The notice is transient: remounting or refreshing does not replay it.
|
||||
|
||||
<a id="use-this-package"></a>
|
||||
## Use this package
|
||||
|
||||
@@ -57,7 +59,7 @@ A terminal account-state stream failure appears in the sign-in dialog or Account
|
||||
The balance card shows recharge funds and positive bonus credit in separate rows. Empty or nonpositive bonus wallets hide the bonus row and its divider; currencies retain their own amounts.
|
||||
|
||||
|
||||
A successful first login initializes the default model from `deepseek-account` when no user choice has been saved. Later logins retain the saved choice, including an unavailable one.
|
||||
A successful login selects the first available `deepseek-account` model as the default when no other provider has a configured API key, even if a previous default was saved. Configured keys preserve the existing default even when their provider has no available models. Session-specific selections remain unchanged.
|
||||
|
||||
<a id="model-experience"></a>
|
||||
## Model Experience
|
||||
|
||||
@@ -19,6 +19,8 @@ Desktop 设置中的账号页面显示 DeepSeek 登录状态,并提供浏览
|
||||
- [模型体验](#model-experience)
|
||||
- [已知限制与后续工作](#known-limitations-and-deferred-work)
|
||||
|
||||
新的账号模型登录要求事件会弹出“当前模型暂不可用,请登录后再试”提示。提示仅保留在当前界面,重新挂载或刷新不会重放。
|
||||
|
||||
<a id="use-this-package"></a>
|
||||
## 使用此包
|
||||
|
||||
@@ -57,7 +59,7 @@ Desktop 的用量和充值操作在 48px 返回栏下方打开隔离的原生 Pl
|
||||
余额卡片分行展示充值余额与正额赠金余额。赠送钱包为空或没有正余额时,隐藏赠金行及其分隔线;不同币种分别显示。
|
||||
|
||||
|
||||
首次成功登录且用户尚未保存模型选择时,从 `deepseek-account` 初始化默认模型。后续登录保留已保存的选择,包括不可用的选择。
|
||||
登录成功时,若其他提供方均未配置 API key,则将 `deepseek-account` 的第一个可用模型设为默认值,即使此前保存过默认选择。其他提供方只要配置了 key,就保留原默认值,即使其没有可用模型。会话内的显式选择保持不变。
|
||||
|
||||
<a id="model-experience"></a>
|
||||
## 模型体验
|
||||
|
||||
@@ -19,12 +19,15 @@ export type AccountMenuProps = PropsRuntime<'settings.launcher'> & PropsLocale<'
|
||||
* @returns account menu launcher.
|
||||
*/
|
||||
export function AccountMenu({
|
||||
wide, openSettings, openOnboarding, useAccount, useTheme, signOut, hasRunningAccountTasks, contactUs, showLogin, start, cancel, t,
|
||||
subscribeModelSignInRequired, wide, openSettings, openOnboarding, useAccount, useTheme, signOut, hasRunningAccountTasks,
|
||||
contactUs, showLogin, start, cancel, t,
|
||||
}: AccountMenuProps) {
|
||||
const account = useAccount(state => state)
|
||||
const colorScheme = useTheme(snapshot => snapshot.active.colorScheme)
|
||||
const signedIn = account.view?.status === 'credential-stored'
|
||||
const expired = account.view?.status === 'signed-out' && account.view.signOutReason === 'expired'
|
||||
const [signInNotice, setSignInNotice] = useState(0)
|
||||
useEffect(() => subscribeModelSignInRequired?.(() => { setSignInNotice(value => value + 1) }), [subscribeModelSignInRequired])
|
||||
const [expiryNotice, setExpiryNotice] = useState(false)
|
||||
useEffect(() => { setExpiryNotice(expired) }, [expired])
|
||||
const profile = account.details?.profile
|
||||
@@ -42,6 +45,7 @@ export function AccountMenu({
|
||||
// The plugin's start publishes `loginFailed` before it rejects, so the dialog owns the report.
|
||||
const beginSignIn = (): void => { setOpen(false); void start().catch(() => undefined) }
|
||||
return <div className={css.root}>
|
||||
{signInNotice > 0 && <Toast key={signInNotice} text={t('modelSignInRequired')} onDone={() => { setSignInNotice(0) }} />}
|
||||
{expiryNotice && <Toast text={t('sessionExpired')} onDone={() => { setExpiryNotice(false) }} />}
|
||||
<Menu open={open} side="top" portal autoFocus className={css.anchor} listClassName={signedIn ? undefined : css.signedOutMenu}
|
||||
anchor={<button type="button" className={css.trigger} data-collapsed={!wide} data-signed-out={!signedIn} aria-label={t('menu')}
|
||||
|
||||
@@ -29,6 +29,11 @@ export interface AccountSnapshot {
|
||||
|
||||
/** Host operations injected into the Cordis-free account component. */
|
||||
export interface AccountSectionInjected {
|
||||
/** Subscribe to live model sign-in guidance; the returned function removes the listener.
|
||||
* @param listener - callback for one rejected account-model request.
|
||||
* @returns listener cleanup.
|
||||
*/
|
||||
subscribeModelSignInRequired?: (listener: () => void) => () => void
|
||||
/** Desktop-only commands; absent in ordinary browsers. */
|
||||
platform?: PlatformBridge
|
||||
|
||||
|
||||
@@ -89,6 +89,7 @@ export function apply(ctx: Context): void {
|
||||
})().catch(() => { if (!disposed) publish({ ...snapshot, failed: true }) })
|
||||
const nativePlatform = (globalThis as typeof globalThis & { dshPlatform?: PlatformBridge }).dshPlatform
|
||||
const operations: AccountSectionInjected = {
|
||||
subscribeModelSignInRequired: listener => ctx.remote.$on('deepseek-account/model-sign-in-required', listener),
|
||||
...nativePlatform === undefined ? {} : { platform: nativePlatform },
|
||||
refresh,
|
||||
contactUs() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/** Account settings copy, owned by the account feature. */
|
||||
export const en = {
|
||||
modelSignInRequired: 'Model unavailable. Please sign in and try again.',
|
||||
sessionExpired: 'You have signed out of your account, please log in again.',
|
||||
close: 'Close', addApiKey: 'Add API Key', retry: 'Sign in again',
|
||||
loginTitle: 'Start creating', loginDescription: 'Sign in to create, edit, and share your design projects. Everything is saved locally.',
|
||||
@@ -29,6 +30,7 @@ export const en = {
|
||||
export type AccountKey = keyof typeof en
|
||||
/** Chinese account settings copy. */
|
||||
export const zh: Record<AccountKey, string> = {
|
||||
modelSignInRequired: '当前模型暂不可用,请登录后再试',
|
||||
sessionExpired: '登录信息已失效,请重新登录',
|
||||
close: '关闭', addApiKey: '添加 API Key', retry: '重新登录',
|
||||
loginTitle: '开始你的创作', loginDescription: '登录后即可创建、编辑和分享你的设计项目,所有内容在本地保存。',
|
||||
|
||||
@@ -491,3 +491,25 @@ it('reports resize failure, ignores late native failures, and tolerates a remove
|
||||
await act(async () => { loaded.reject(new Error('late failure')); await loaded.promise.catch(() => {}) })
|
||||
expect(screen.queryByRole('dialog')).toBeNull()
|
||||
})
|
||||
|
||||
it.each([en, zh])('shows live model sign-in guidance without replaying it after remount', async (copy) => {
|
||||
const operations = operationsOf({ status: 'signed-out', attempt: null })
|
||||
let listener: (() => void) | undefined
|
||||
const unsubscribe = vi.fn(() => { listener = undefined })
|
||||
operations.subscribeModelSignInRequired = (next) => { listener = next; return unsubscribe }
|
||||
const { AccountMenu } = await import('../src/client/AccountMenu.tsx')
|
||||
const element = <AccountMenu {...({} as GlobalStandardProps)} {...operations}
|
||||
useAccount={selector => selector(operations.hooks.account.getSnapshot())}
|
||||
useTheme={selector => selector(operations.hooks.theme.getSnapshot())} wide openOnboarding={() => {}} openSettings={() => {}}
|
||||
t={key => key in copy ? copy[key as AccountKey] : key} />
|
||||
const view = render(element)
|
||||
expect(screen.queryByRole('alert')).toBeNull()
|
||||
act(() => { listener?.() })
|
||||
expect(screen.getByRole('alert').textContent).toBe(copy.modelSignInRequired)
|
||||
act(() => { listener?.() })
|
||||
expect(screen.getAllByRole('alert')).toHaveLength(1)
|
||||
view.unmount()
|
||||
expect(unsubscribe).toHaveBeenCalledOnce()
|
||||
render(element)
|
||||
expect(screen.queryByRole('alert')).toBeNull()
|
||||
})
|
||||
@@ -51,6 +51,7 @@
|
||||
"@deepseek-ai/dsh-home-paths": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm-deepseek": "workspace:*",
|
||||
"@deepseek-ai/dsh-llm-deepseek-api-key": "workspace:*",
|
||||
"@deepseek-ai/dsh-session": "workspace:*",
|
||||
"@deepseek-ai/dsh-session-projection": "workspace:*",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:*",
|
||||
|
||||
@@ -8,7 +8,7 @@ import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
|
||||
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
|
||||
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek-api-key'
|
||||
import * as AgentInstructions from '@deepseek-ai/dsh-agent-instructions'
|
||||
import { candidateScopeKey } from '../src/render.ts'
|
||||
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
|
||||
|
||||
@@ -51,7 +51,6 @@ function sessionAgent(session: Session, id = 'agent'): Agent {
|
||||
options: {},
|
||||
session,
|
||||
inbox: unsupportedInbox(),
|
||||
activeProvider: undefined,
|
||||
status: 'running',
|
||||
ctx: new Context(),
|
||||
send: () => {},
|
||||
|
||||
@@ -110,7 +110,6 @@ function sessionAgent(session: Session, id = 'agent'): Agent {
|
||||
options: {},
|
||||
session,
|
||||
inbox: unsupportedInbox(),
|
||||
activeProvider: undefined,
|
||||
status: 'running',
|
||||
ctx: new Context(),
|
||||
send: () => {},
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/core/agent-default-model/README.md
|
||||
README.md: 376698f309e311f979a01f39feb1f84a71751713
|
||||
README.zh.md: b24b0925504f45553d71e2f2594f359daa29983c
|
||||
README.md: 2aa3d8ed4e4f36286226f76df4c21dd5cb87be0d
|
||||
README.zh.md: 7283a44f5f491c1fd51d4c33b6df3f65cb8e76bf
|
||||
@@ -7,7 +7,7 @@ kind: "package-reference"
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
`initializeSelection()` serializes the first setup choice with user writes and preserves an explicit profile selection, including unavailable models.
|
||||
`initializeSelection()` serializes the first setup choice with user writes and checks the persisted profile override before writing. It preserves explicit selections, including unavailable models.
|
||||
|
||||
## Summary
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ kind: "package-reference"
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
`initializeSelection()` 将首次配置选择与用户写入串行处理,并保留显式的 profile 选择,包括暂不可用的模型。
|
||||
`initializeSelection()` 将首次配置选择与用户写入串行处理,并在写入前检查持久化的 profile override。它保留显式选择,包括暂不可用的模型。
|
||||
|
||||
## 概述
|
||||
|
||||
|
||||
@@ -52,7 +52,6 @@ export class AgentDefaultModelConfig extends Service {
|
||||
reasoningEffort: z.string().volatile(),
|
||||
})
|
||||
|
||||
private saved = false
|
||||
private writes: Promise<void> = Promise.resolve()
|
||||
|
||||
constructor(private readonly ownerContext: Context, private config: Config) {
|
||||
@@ -98,12 +97,11 @@ export class AgentDefaultModelConfig extends Service {
|
||||
const editor = this.ctx.get('configEditor')
|
||||
if (entry === undefined || editor === undefined) return
|
||||
const override = editor.configuration().find(row => row.entry === entry)?.override
|
||||
if (initialize && (this.saved || override !== undefined && ('provider' in override || 'model' in override))) return
|
||||
if (initialize && override !== undefined && ('provider' in override || 'model' in override)) return
|
||||
await editor.edit(entry, () => ({
|
||||
provider: next.provider, model: next.model,
|
||||
...next.reasoningEffort === undefined ? {} : { reasoningEffort: String(next.reasoningEffort) },
|
||||
}))
|
||||
this.saved = true
|
||||
})
|
||||
this.writes = operation.catch(() => { /* A refused write does not block the next user choice. */ })
|
||||
return operation
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/core/agent-loop/README.md
|
||||
README.md: c83a0ee287297efba4bc20b2fb8e158e57d87e27
|
||||
README.zh.md: 55fbc69270fb4bccc6c54b3738c1e5d94db8f059
|
||||
README.md: 07033fb3204e827c5227a99e7211e5e648c389c6
|
||||
README.zh.md: 0fce6147b37d704add8266deaf185ea83ae84b73
|
||||
@@ -25,7 +25,6 @@ English | [中文](README.zh.md)
|
||||
<a id="use-this-package"></a>
|
||||
## Use this package
|
||||
|
||||
The running phase retains the final prepared provider through tool execution and retry preparation, resets it before publishing `turn/end`, and exposes it through `Agent.activeProvider`. Plugins can cancel matching live agents without consulting mutable settings or historical request headers.
|
||||
|
||||
Mount `dsh-agent-loop` in any composition that should run agents. It supplies the driver behind `ctx.agents` and starts any agents you declare in its config; both [`dsh-base`](../../bundle/base/README.md) and [`dsh-sdk-minimal`](../../bundle/sdk-minimal/README.md) mount it as an explicit row.
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ kind: "package-reference"
|
||||
<a id="use-this-package"></a>
|
||||
## 使用本包
|
||||
|
||||
运行阶段在工具执行及重试准备期间保留最终准备完成的提供方,在发布 `turn/end` 前重置,并通过 `Agent.activeProvider` 暴露。插件可以取消匹配的运行中代理,无须读取可变设置或历史请求头。
|
||||
|
||||
在任何应运行 agent 的组合中挂载 `dsh-agent-loop`。它提供 `ctx.agents` 背后的驱动器,并启动你在配置中声明的 agent;[`dsh-base`](../../bundle/base/README.zh.md) 与 [`dsh-sdk-minimal`](../../bundle/sdk-minimal/README.zh.md) 都将它作为显式配置行挂载。
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-llm-deepseek-api-key": "workspace:*",
|
||||
"@deepseek-ai/cordis": "workspace:~",
|
||||
"@deepseek-ai/dsh-llm-pi-ai": "workspace:*",
|
||||
"@deepseek-ai/dsh-agent": "workspace:*",
|
||||
|
||||
@@ -46,7 +46,7 @@ type Phase =
|
||||
lastTurn: number
|
||||
wakeRequested: boolean
|
||||
}
|
||||
| { kind: 'running'; abort: AbortController; turn: number; step: number; wakeRequested: boolean; activeProvider?: string }
|
||||
| { kind: 'running'; abort: AbortController; turn: number; step: number; wakeRequested: boolean }
|
||||
|
||||
type StepEndReason = Extract<TurnEndReason, { kind: 'completed' | 'max-tokens' }>
|
||||
|
||||
@@ -140,10 +140,6 @@ export class ReactLoopAgent implements Agent {
|
||||
return this.phase.kind === 'idle' || this.phase.kind === 'maintenance' ? 'idle' : 'running'
|
||||
}
|
||||
|
||||
get activeProvider(): string | undefined {
|
||||
return this.phase.kind === 'running' ? this.phase.activeProvider : undefined
|
||||
}
|
||||
|
||||
/** Commit a phase and publish its externally visible status transition. */
|
||||
private setPhase(next: Phase): void {
|
||||
const previousStatus = this.status
|
||||
@@ -367,7 +363,6 @@ export class ReactLoopAgent implements Agent {
|
||||
this.throwError(error)
|
||||
} finally {
|
||||
try {
|
||||
delete phase.activeProvider
|
||||
// oxlint-disable-next-line typescript/no-non-null-assertion -- every exit assigns a turn ending
|
||||
this.session.append('turn/end', { turn, reason: turnEnds! })
|
||||
} catch (error: unknown) {
|
||||
@@ -579,7 +574,6 @@ export class ReactLoopAgent implements Agent {
|
||||
config = proposedConfig
|
||||
}
|
||||
signal.throwIfAborted()
|
||||
if (this.phase.kind === 'running') this.phase.activeProvider = config.provider
|
||||
return { config, ...preparedCall === undefined ? {} : { preparedCall } }
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ function stubAgent(rawId: string, overrides: Partial<Agent> = {}): Agent {
|
||||
options: {},
|
||||
session,
|
||||
inbox: unsupportedInbox(),
|
||||
activeProvider: undefined,
|
||||
status: 'idle',
|
||||
ctx,
|
||||
send: () => {},
|
||||
|
||||
@@ -9,7 +9,7 @@ import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
|
||||
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
|
||||
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
|
||||
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek-api-key'
|
||||
|
||||
/**
|
||||
* With-key proof that log-derived requests translate into real provider cache hits: a
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/core/agent/README.md
|
||||
README.md: 3d88527a3fed66e2a7bc90ba11ad9c52df0fedec
|
||||
README.zh.md: cb603d265d9a430f063fcab8024e61f2a8ef7389
|
||||
README.md: 17ee5fd723f5322c0ccdfd7cc8443dbb842cf674
|
||||
README.zh.md: 94424a5e9bc5a30fbbb5e775658c7b90a37f24f7
|
||||
@@ -25,7 +25,6 @@ Use `dsh-agent` to create or resume live agents, send follow-up or steering inpu
|
||||
<a id="use-this-package"></a>
|
||||
## Use this package
|
||||
|
||||
The read-only `Agent.activeProvider` identifies the current turn’s latest prepared request. It is absent before the first request is prepared and between turns; tools and retries retain the last provider until another request finishes preparation. Configuration changes alone do not reclassify in-flight work.
|
||||
|
||||
Mount `dsh-agent` wherever live agents exist: it provides `ctx.agents` and the `Agent` handle that plugins, UI, hooks, and orchestrators work against. The service is inert until a driver registers a factory — the shipped driver is `dsh-agent-loop`, so the smallest useful composition loads both.
|
||||
|
||||
|
||||
Loaded 100 of 302 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user