dsh-bailinghub 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +7 -8
  3. package/docs/AGENT_CLIENT_CONTRACT.md +25 -5
  4. package/docs/AGENT_KNOWLEDGE.md +62 -0
  5. package/docs/CAPABILITY_FEEDBACK.md +32 -3
  6. package/docs/COMPATIBILITY.md +8 -8
  7. package/docs/CONTEXT_DELIVERY.md +133 -0
  8. package/docs/GETTING_STARTED.md +4 -4
  9. package/docs/GETTING_STARTED.zh-CN.md +4 -4
  10. package/docs/MIGRATION_VNEXT.md +2 -4
  11. package/docs/README.zh-CN.md +7 -8
  12. package/docs/RELEASE_NOTES_v0.7.0.en.md +15 -0
  13. package/docs/RELEASE_NOTES_v0.7.0.md +15 -0
  14. package/docs/RELEASE_NOTES_v0.8.0.en.md +21 -0
  15. package/docs/RELEASE_NOTES_v0.8.0.md +21 -0
  16. package/docs/SESSION_TOOL_REUSE.md +50 -0
  17. package/docs/UPGRADE_v0.7.0.en.md +15 -0
  18. package/docs/UPGRADE_v0.7.0.md +15 -0
  19. package/docs/UPGRADE_v0.8.0.en.md +22 -0
  20. package/docs/UPGRADE_v0.8.0.md +22 -0
  21. package/docs/USAGE_SERVICE.md +96 -0
  22. package/lib/capability-feedback.js +25 -4
  23. package/lib/context-delivery.js +135 -0
  24. package/lib/execution-summary.js +45 -0
  25. package/lib/index.js +9 -0
  26. package/lib/invocation-journal.js +5 -0
  27. package/lib/knowledge.js +105 -0
  28. package/lib/model-gateway-transport.js +246 -0
  29. package/lib/runtime.js +198 -24
  30. package/lib/session-tool-cache.js +1 -1
  31. package/lib/session-tools.js +50 -15
  32. package/lib/session-usage-store.js +280 -0
  33. package/lib/tool-guidance.js +27 -0
  34. package/lib/transport.js +19 -0
  35. package/lib/usage-llm-stream.js +157 -0
  36. package/lib/usage-model-transport.js +206 -0
  37. package/lib/usage-provider-stream.js +82 -0
  38. package/package.json +21 -3
@@ -87,6 +87,32 @@ If a ready tool's full schema is outside the presentation window, use the exact
87
87
  come from local cache, while normal scope validation may still make requests.
88
88
  The model must not guess fields or rely on an old schema that it cannot inspect.
89
89
 
90
+ Short descriptions and usage guidance have separate purposes. A tool can report
91
+ `description_truncated=true` or `usage_guidance_available=true`. Read the indicated
92
+ detail explicitly before relying on omitted instructions:
93
+
94
+ ```json
95
+ {
96
+ "query": "Read the known decoration tool's prerequisites.",
97
+ "tool_name": "shop_ai_decoration_save",
98
+ "include_usage_guidance": true,
99
+ "authorization_ref": "the-original-selected-authorization"
100
+ }
101
+ ```
102
+
103
+ `include_usage_guidance` requires `tool_name`; multi-target sessions also select
104
+ one authorization. The same parameters are supported in `active_turn` mode.
105
+ Only this named tool's guidance is fetched, under the current authorization and
106
+ capability revision. Its `usage_guidance` result contains the original name,
107
+ authorization reference where applicable, revision, text, and honest character/
108
+ UTF-8 byte bounds (`max_chars=32768`, `max_bytes=65536`, with `truncated` and source
109
+ sizes). A truncated detail is not a complete instruction document. The text is
110
+ reference evidence, never permission to switch target, bypass approval or execute
111
+ unvalidated arguments. Detail reads create no invocation, approval or business
112
+ operation. Unsupported older SDKs return compatibility feedback; missing support
113
+ does not mean that the business capability is absent. Hosts must preserve this
114
+ field and its bounds instead of treating the short description as full guidance.
115
+
90
116
  An exact valid cache hit avoids an additional `searchCapabilities` request. The
91
117
  first preparation of a target still calls `startTurn` for current context; Core
92
118
  may perform its existing bounded internal discovery there. This is reduced
@@ -126,6 +152,30 @@ prepares that exact target and declaration, then returns context and schema with
126
152
  business arguments. The model must read the result and decide on a new tool call
127
153
  using the current instructions and schema.
128
154
 
155
+ Session preparation now has an explicit additive envelope:
156
+
157
+ ```json
158
+ {
159
+ "schema": "bailing.agent-capability-preparation.v1",
160
+ "result_type": "capability_preparation",
161
+ "dispatch": "not_dispatched",
162
+ "reason": "target_not_prepared",
163
+ "next_action": "review_context_then_call",
164
+ "preparation": { "state": "ready", "business_operation_performed": false },
165
+ "business_operation_performed": false
166
+ }
167
+ ```
168
+
169
+ Normal discovery uses `reason=capability_search`. A ready target with no returned
170
+ requested schema says `next_action=refine_capability_search`; it does not advise
171
+ calling the missing tool. An unavailable preparation carries its safe feedback's
172
+ next action (or `review_preparation_feedback` for mixed outcomes). Preparation
173
+ creates no `invocation_id`. The existing context, schemas, target state and both
174
+ false flags remain compatible. Business invocation success keeps its existing
175
+ receipt shape. Active-turn search also adds `result_type`, `dispatch`, `reason`,
176
+ `next_action` and `business_operation_performed=false`, while preserving its
177
+ existing search schema and result fields.
178
+
129
179
  The original guard call ID stays attached to preparation. Replaying that same
130
180
  call ID returns its preparation result, rather than turning the old attempt into
131
181
  a write after preparation finishes. A later business call needs its own genuine
@@ -0,0 +1,15 @@
1
+ # Upgrade to DSH 0.7.0
2
+
3
+ Upgrade with the matched Core 0.9.0 / SDK 0.7.0 / DSH 0.7.0 release set. Existing public 0.6.0 business-governance integrations remain supported; managed model plans require explicit host integration.
4
+
5
+ 1. Back up and apply the Core 0.9.0 upgrade migrations. Enabling this optional module does not recreate business authorizations.
6
+ 2. Install exact paired packages. DSH 0.7.0 depends on SDK 0.7.0. Restart the host and verify its actual Profile resolution.
7
+ 3. Exchange a Usage credential through the trusted backend, preserving plan-wide or single-service scope. Provider keys stay in Core. Read capabilities, modelModels, modelTools and modelSummary; never substitute a default model or authorization for an empty catalog.
8
+ 4. Keep orchestration local, provide complete tool schemas, use transport.stream for plan models and runModelTool for generation. Persist the original operation ID before dispatch. Late responses cannot reactivate cancelled/ended turns or override storage failures.
9
+ 5. Verify streaming, image results, shared allowance and original-request recovery. A complete result is usable while billing is pending; do not reissue it or block unrelated new user messages for settlement.
10
+
11
+ Display server presentation. Periodic allowance is grant.periodAllowanceUsd, not sale price, token totals or model count. Refresh summary after an administrator resets allowance; do not recreate accounts or credentials.
12
+
13
+ Model billing is new relative to public 0.6.0 and provides no legacy Token-quota aliases. No history or account purge is required. Upgrading public Core 0.8.0 only adds outstanding 063/064 migrations; maintaining test configuration is not a real-customer upgrade step. Existing business Sessions and original invocation recovery remain unchanged.
14
+
15
+ Respect callable=false for unsupported video/audio adapters. Inspect uncertain requests by original ID only. See the [contract](USAGE_SERVICE.md).
@@ -0,0 +1,15 @@
1
+ # DSH 0.7.0 升级
2
+
3
+ 使用 Core 0.9.0 / SDK 0.7.0 / DSH 0.7.0 配套升级。公开 0.6.0 的业务治理接入继续保持,可选模型套餐需宿主显式接入。
4
+
5
+ 1. 按 Core 0.9.0 升级指南完成备份和新增迁移。未启用套餐时不需要重建原业务授权。
6
+ 2. 安装精确配套包;DSH 0.7.0 固定依赖 SDK 0.7.0。重启宿主并核对实际 Profile 加载的版本,不能只检查开发目录。
7
+ 3. 可信业务后端签发使用凭据,明确套餐范围或单模型范围;供应商密钥保留在 Core。读取 capabilities、modelModels、modelTools、modelSummary。无可用项时显示配置原因,不回退到其他模型或授权。
8
+ 4. 宿主继续本地编排,将完整工具 schema 提供给模型;套餐使用 `stream`,生成工具使用 `runModelTool`。持久保存原 operation ID,取消/存储失败/已结束轮次不能被迟到结果重新启用。
9
+ 5. 核对文本流式、图片结果、同一套餐额度及原请求恢复。`result_state=complete` 即可使用结果,`billing_state=pending` 不要求重复请求或阻止下一条独立用户消息。
10
+
11
+ 额度显示直接读取 presentation;周期剩余按原 grant 的 periodAllowanceUsd,而非售价、Token 数或模型数量计算。后台手动重置后刷新摘要即可,无需重建账户或凭据。
12
+
13
+ 模型计费是相对公开 0.6.0 的新增模块,不提供旧 Token 额度接口别名,也不要求清退历史或账户。从公开 Core 0.8.0 升级仅应用新增 063/064 迁移;测试配置的维护不属于真实客户升级步骤。已有业务 Session 和原 invocation 恢复规则不变。
14
+
15
+ 视频/语音不含可执行适配时必须尊重 callable=false。断网或不确定请求只按原 ID 核对,不重新派发生成。详见[契约](USAGE_SERVICE.md)。
@@ -0,0 +1,22 @@
1
+ # Upgrade to DSH 0.8.0
2
+
3
+ Pair Core 0.10.0, SDK 0.8.0 and DSH 0.8.0. Node.js remains `^22.19.0 || >=24.0.0`; the exercised Harness line remains `0.1.1-rc.2`. The four public configuration fields are unchanged.
4
+
5
+ 1. Back up and upgrade Core first, applying only its outstanding migrations. The plugin does not migrate the Hub database.
6
+ 2. Checkpoint active tasks and preserve pending original operations/approvals, credentials, Sessions, scope/task/invocation stores and archives. Never reconstruct an unknown business action.
7
+ 3. Install `dsh plugin --profile web add dsh-bailinghub@0.8.0`. It must resolve exact `bailinghub-mcp-server@0.8.0` automatically; do not substitute an optional peer, local path or another SDK version.
8
+ 4. Restart and verify the actual Profile and loaded modules. Run `/bailinghub doctor` and `/bailinghub status` without printing credentials. Retain explicit authorization scope and business rules.
9
+
10
+ ## Host integration
11
+
12
+ - Session tool reuse still requires explicit `toolLifecycle:'session'`. Calling an unprepared tool returns preparation only. Read the current context and complete schema before a new decision; never turn replay of the old call ID into execution.
13
+ - Current authorized protocol negotiation enables on-demand knowledge. Pin the original turn mode and use `search_business_knowledge` / `read_business_knowledge`, retaining ref, run, authorization_ref and pagination/truncation facts. Unknown protocols retain inline knowledge; service/auth failures cannot silently discard it. See [the knowledge protocol](AGENT_KNOWLEDGE.md).
14
+ - Integrate context confirmation only when the host can observe every final provider request and confirm its current named contributions. Keep inline bodies without confirmation. If a later request is missing context, reassemble/reconfirm or stop; never replay business calls to repair prompts. See [the host contract](CONTEXT_DELIVERY.md).
15
+ - Model descriptors pass through unchanged. Hosts may use SDK `resolveModelOutputBudget()` and `modelOutputOptions()` to separate defaults/ceilings, output keys and scope. Apply them to new requests only; existing operation/intent/hash remain unchanged.
16
+ - Automatic continuation needs a separate host implementation with final original receipts, checkpoints, bounded budgets, cancellation, approval and effect deduplication. Inspect missing final receipts with `inspectModelRequest()`; inspect business receipts with `inspectInvocation()`. `resume()` can first-dispatch an approved original and is not polling.
17
+
18
+ ## Acceptance and fallback
19
+
20
+ Use an isolated Profile for single-plugin installation, one exact SDK resolution, normal tools, preparation without dispatch, knowledge search/pagination, authorization isolation, cancel/revoke and original recovery. Context confirmation requires checking final request contents. Each host separately accepts its real UI and long tasks.
21
+
22
+ The immutable Client Token 0.1.1 static path remains independent. Package rollback does not roll back Hub data or rewrite saved operations. Preserve new records and do not continue Sessions in an old host that lacks their task/recovery formats. Audit the actual installing host separately.
@@ -0,0 +1,22 @@
1
+ # 升级到 DSH 0.8.0
2
+
3
+ 配套 Core 0.10.0、SDK 0.8.0 与 DSH 0.8.0。Node.js 保持 `^22.19.0 || >=24.0.0`,Harness 配套保持 `0.1.1-rc.2`,原四项公开配置不变。
4
+
5
+ 1. 管理员先备份并升级 Core,仅执行其尚未完成的迁移;插件不迁移中枢数据库。
6
+ 2. 保存当前任务检查点、原待决操作和审批;保留凭据、Session、scope、task、invocation 及归档存储。不要重建原未知业务动作。
7
+ 3. 安装 `dsh plugin --profile web add dsh-bailinghub@0.8.0`。插件必须自动解析精确 `bailinghub-mcp-server@0.8.0`;不使用 optional peer、本地路径或另一个 SDK 版本替代。
8
+ 4. 重启并核对实际 Profile 和已加载模块。先运行 `/bailinghub doctor`、`/bailinghub status`,不输出凭据。保持原显式授权范围和业务规则。
9
+
10
+ ## 宿主接入
11
+
12
+ - 会话工具复用仍显式启用 `toolLifecycle:'session'`。直接调用未准备工具只返回准备信息;读取其当前上下文与完整 schema 后再作新决策,不能把旧 callId 自动重放成执行。
13
+ - 按需知识由当前授权下的协议协商开启。固定原 turn 模式,使用 `search_business_knowledge`、`read_business_knowledge`;保留原 ref、run、authorization_ref 与分页/截断事实。未知协议保留旧 inline;服务和授权错误不能默默删掉知识。[知识协议](AGENT_KNOWLEDGE.md)
14
+ - 只有宿主能观察每一份最终 provider 请求并确认当前命名上下文时,才接入上下文确认。未经确认保留 inline;确认后若下一请求缺少上下文,应重新组装/确认或停止,不重复业务调用。[宿主确认契约](CONTEXT_DELIVERY.md)
15
+ - 模型目录保持原样透传。宿主可使用 SDK `resolveModelOutputBudget()`、`modelOutputOptions()` 区分默认与服务上限、输出字段和 scope;只用于新请求,旧 operation/intent/hash 不变。
16
+ - 自动续跑需要宿主独立实现:最终原回执、检查点、有限预算、取消、审批和副作用防重。缺最终回执先 `inspectModelRequest()`;业务核对用 `inspectInvocation()`,`resume()` 可能首次派发原已批准调用,不能当轮询。
17
+
18
+ ## 验收与回退
19
+
20
+ 先在隔离 Profile 检查单插件安装、SDK 唯一解析、普通工具执行、准备不派发、知识搜索/分页、多授权隔离、取消/撤权及原操作恢复。上下文确认需核最终请求内容;不同宿主的真实 UI 与长任务仍分别验收。
21
+
22
+ 旧 Client Token 0.1.1 静态路径独立保留,不改历史配置。回退包不回滚中枢数据,也不改写已保存原操作;保留新记录,不能把需要新任务/恢复格式的会话交给不支持的旧宿主继续执行。依赖审计按实际安装宿主另行核对。
@@ -0,0 +1,96 @@
1
+ # USD model gateway for local agents
2
+
3
+ A plan has a USD allowance, one multiplier, and a live selection of conversation models and model tools. The Hub relays individual requests and meters their cost. The host keeps its original planning, tool selection and AgentLoop. Business authorization, approval, session scope, task controls and conversation archives remain separate.
4
+
5
+ The current release pairs Core 0.10.0, SDK 0.8.0 and DSH 0.8.0. It has no legacy Token-allowance aliases, silent billing fallback, balance conversion or automatic BYOK substitution. The optional gateway first shipped with Core 0.9.0 and SDK/DSH 0.7.0; its original upgrade from Core 0.8.0 required outstanding migrations 063/064. For the current release, follow the Core upgrade guide and apply only outstanding migrations while retaining existing data. Do not erase business authorizations, history or unrelated sessions; abandoned test configuration is separate maintenance, not a customer upgrade requirement.
6
+
7
+ ## Identity and catalog
8
+
9
+ The trusted product backend issues a Usage credential for the original Hub/user/account. `model_access:'token_gateway'` remains the credential scope spelling for the whole permitted plan; it does not select a Token billing algorithm. A deliberately single-service credential stays restricted to its original service. Supplier URLs and API keys stay server-side.
10
+
11
+ ```js
12
+ import { createUsageClient } from 'bailinghub-mcp-server/usage'
13
+ const client = createUsageClient({
14
+ hubUrl: trustedLogin.hubUrl,
15
+ userId: trustedLogin.userId,
16
+ accountId: trustedLogin.accountId,
17
+ serviceId: trustedLogin.serviceId,
18
+ accessTokenProvider: () => secureCredentials.readUsageToken(),
19
+ })
20
+ await client.capabilities()
21
+ const models = await client.modelModels()
22
+ const tools = await client.modelTools()
23
+ const summary = await client.modelSummary()
24
+ ```
25
+
26
+ Capabilities must advertise `model_gateway` with schema `bailing.model-gateway.v1`, `billing_unit:'USD'`, `orchestration:'host'`, `turn_required:false`, streaming, native provider response envelopes and asynchronous settlement. Old Core/SDK returns explicit unsupported, not an alternative billing path.
27
+
28
+ `modelModels()` reads `/usage/v1/model/models`, schema `bailing.model-models.v1`. Its `items` are only conversation models. Render the operator's `label`, store the immutable `service_id`, never route by label or provider model name. Every read refreshes current plan membership. An empty directory is valid and does not authorize choosing an unrelated model.
29
+
30
+ `modelTools()` reads `/usage/v1/model/tools`, schema `bailing.model-tools.v1`. Each item has a stable `service_id`, `service_revision`, display label, capability/output kinds and a `tool:{name,description,input_schema}`. Register only `callable:true` items in the host's local tool registry. Keep unavailable items as configuration diagnostics with their explicit `reason`. Catalog reads neither generate media nor start business runs. Namespace registrations by service identity to avoid duplicate operator-defined names or collisions with business tools; tool names alone are never authorization identities.
31
+
32
+ ## Shared allowance and display
33
+
34
+ `modelSummary()` returns `bailing.billing-summary.v1`, with USD fields `availableUsd`, `consumedUsd`, `currentPeriodConsumedUsd`, `overageUsd`, the original grant and current plan. The plan supplies `serviceIds` and one `multiplier`; per-model input/output multipliers and Token balances no longer exist.
35
+
36
+ Use `presentation` (`bailing.usage-presentation.v1`) for customer balances: allowance packs show credits; periodic plans show remaining percentage and reset time. Never recalculate balances locally from raw tokens, prices or a previous model response. Missing/malformed presentation is `USAGE_RESPONSE_INVALID`, not a guessed balance. Account details may show reported provider Token counts as usage information, separate from allowance and percentage. If unavailable, show not provided, never fabricate zero Tokens.
37
+
38
+ Reference provider pricing is not an actual supplier invoice or guaranteed margin. Each request snapshots its plan multiplier and reference price; server-side USD accounting is exact to twelve decimal places. Receipt fields `reference_cost_usd`, `billed_usd`, `overage_usd` are amounts; `usage` contains integer Token counts when reported, and `raw_usage` can contain non-Token measurements. Unknown usage keeps billing pending. No client-side multiplication, second debit or extra model request is allowed.
39
+
40
+ ## Request and recovery API
41
+
42
+ | SDK method | Behavior |
43
+ | --- | --- |
44
+ | `modelComplete(input)` | One JSON model request, no server agent loop |
45
+ | `modelStream(input)` | Provider packets followed by the original durable operation receipt |
46
+ | `runModelTool(input)` | One model-tool generation request |
47
+ | `inspectModelRequest(operationId, originalCoordinates)` | Read original operation only |
48
+ | `cancelModelRequest(operationId, originalCoordinates)` | Cancel original operation, never replay it |
49
+
50
+ Conversation requests use `/usage/v1/model/requests` and `/usage/v1/model/requests/stream`. Tool requests use `POST /usage/v1/model/tools/requests` with `{operation_id,service_id,arguments,conversation_id?,turn_id?}`. A tool POST can return HTTP 202 with a durably admitted pending operation. Poll the original request until complete or cancelled; do not submit another generation. All use the same original request inspection/cancellation path and `bailing.model-operation.v1` receipt. Streaming frames use `bailing.model-stream.v1`.
51
+
52
+ A completed model result can have `billing_state:'pending'`; billing settlement must not stop the local loop. Native provider response bytes are preserved in `bailing.provider-response.v1`. The local provider adapter interprets content, finish reason and tool calls. A tool result uses `bailing.model-tool-result.v1` with typed outputs. Downloading or using a result in a business attachment space is an explicit subsequent host action, never an automatic business write by the Hub.
53
+
54
+ Persist the original operation ID, selected service and local event coordinates before any dispatch. If a POST loses its ACK, inspect that original ID; do not create a new ID, regenerate an image, change service, retry through BYOK or label the operation not dispatched. Read/cancel failures preserve uncertainty. Storage failure and recovery gaps remain primary errors.
55
+
56
+ ## Host integration
57
+
58
+ The host must actually register executable model tools; putting their description in a prompt alone is insufficient. Present concise guidance alongside current registered tools: “These are the model tools available under this plan. Use the supplied schemas. When an operation is pending or its result is uncertain, recover the original operation ID rather than repeating generation.”
59
+
60
+ The server still rechecks the live plan and selected service on each new request. A cached descriptor is not a permission grant. Business tool scope and approvals are not bypassed by generating a media file.
61
+
62
+ ## Durable DSH host adapter
63
+
64
+ ```js
65
+ import { createUsageModelTransport, usageNativeModelChunks } from 'dsh-bailinghub'
66
+ const usage = createUsageModelTransport({
67
+ client,
68
+ ensureSessionPersisted: session => persistRealSessionAndConfirm(session),
69
+ getPrimaryStatus: session => currentPersistenceStatus(session),
70
+ })
71
+ const tools = await usage.modelTools()
72
+ const receipt = await usage.runModelTool(realSession, {
73
+ userMessageId: originalUserMessageId,
74
+ modelRequestId: stableLocalToolCallId,
75
+ serviceId: selectedTool.service_id,
76
+ arguments: validatedToolArguments,
77
+ })
78
+ ```
79
+
80
+ Register each callable descriptor using the native host tool registry and route its stable service ID to `usage.runModelTool`. Reuse the same local tool call ID when inspecting an unresolved call. `recoverOperation(session, operationId)` reads the saved original result; it never dispatches a second generation. For a new intentional generation, use a new local call ID.
81
+
82
+ `model({serviceId,refresh?})`, `complete(session,input)`, `stream(session,input)`, `status(session)`, `endTurn`, `cancelTurn`, `completeRequest` and `streamRequest` remain the host transport interfaces. Omit `mode` or use `model_gateway`. The descriptor cache lasts 60 seconds; cache hits do not extend freshness. Use `refresh:true` after settings change. No message-count, paid-turn count or task wall-clock quota is added.
83
+
84
+ The real durable DSH Session indexes `bailinghub/model-request` markers. Each request has a bounded CAS sidecar containing identity/hash/state only, not messages, tool arguments, provider credentials or generated content. Save both before dispatch. Retain them for same-operation recovery; missing metadata produces `USAGE_RECOVERY_GAP`, never an invented request. Old unpublished Token billing markers are not imported into the new ledger; ordinary business session history remains intact.
85
+
86
+ Native streaming uses `usageNativeModelChunks` with the host's actual provider parser. Text/reasoning may display incrementally, but executable tool blocks and finish wait for the original durable receipt. Ending/cancelling the user input immediately fences late results; they remain inspectable history and do not restart tools. Final local save failures remain `storage_error` even when the provider result is known. Unknown billing settlement alone does not make a complete model result unusable.
87
+
88
+ Synthetic acceptance covers current account catalog isolation, empty/unavailable tools, generation ACK loss across full store reopen, cancellation before late generation, raw Token usage alongside USD amounts, native provider streaming, original request recovery and long local tasks.
89
+
90
+ ### Image provider outcome receipts
91
+
92
+ `result_state=failed`, `state=failed`, `dispatch=rejected` is a confirmed provider rejection. Stop waiting; show the bounded `error` and `next_action=contact_operator`. Never automatically create a replacement operation. `unknown` is not a running task or proof of zero cost: preserve the original ID for inspection. Only `pending` means keep waiting. A complete result is usable independently of pending billing.
93
+
94
+ The diagnostic whitelist is `code`, fixed safe `message`, optional `http_status`, `provider_code`, `provider_request_id`, `retryable=false`, and `next_action`. Provider bodies and credentials are excluded. Cancelled/ended turns never regain active tools through late receipts. Existing durable request identities and prior unknown records remain unchanged.
95
+
96
+ DSH stores failed requests using the existing local `rejected` state and returns `delivery_state=request_failed`. Unknown receipts return `unresolved_original`; pending receipts retain `pending_original`. No Session event or sidecar migration is required, and primary storage/recovery errors retain priority.
@@ -1,8 +1,8 @@
1
1
  // The SDK owns HTTP error classification. This projection also supports older
2
2
  // SDKs and custom transports without importing an optional SDK entry point.
3
3
  const ID = /^[0-9a-f]{64}$/
4
- const CATEGORIES = new Set(['tool_not_loaded', 'capability_changed', 'transport_unavailable', 'authorization_unavailable', 'unsupported', 'invocation_outcome_unknown', 'unknown_failure', 'cancelled', 'invalid_request', 'storage_error', 'invocation_recovery_unavailable', 'task_control'])
5
- const ACTIONS = new Set(['rediscover', 'retry_discovery', 'restore_scope', 'reauthorize', 'check_compatibility', 'resume_original', 'inspect_original', 'restore_invocations', 'restore_task', 'inspect_task', 'none'])
4
+ const CATEGORIES = new Set(['tool_not_loaded', 'capability_changed', 'transport_unavailable', 'authorization_unavailable', 'unsupported', 'invocation_outcome_unknown', 'unknown_failure', 'cancelled', 'invalid_request', 'storage_error', 'invocation_recovery_unavailable', 'task_control', 'knowledge_reference_unavailable'])
5
+ const ACTIONS = new Set(['rediscover', 'retry_discovery', 'restore_scope', 'reauthorize', 'check_compatibility', 'resume_original', 'inspect_original', 'restore_invocations', 'restore_task', 'inspect_task', 'search_knowledge', 'none'])
6
6
  const ORIGINS = new Set(['core', 'sdk', 'mcp', 'dsh', 'host'])
7
7
  const CODES = new Set([
8
8
  'TASK_DISPATCH_UNCERTAIN', 'TASK_NOT_FOUND', 'TASK_RUN_INACTIVE', 'TASK_INVALID_INPUT',
@@ -19,6 +19,7 @@ const CODES = new Set([
19
19
  'agent_request_cancelled', 'agent_transport_unavailable', 'agent_request_timeout',
20
20
  'agent_authorization_unavailable', 'agent_schema_unsupported', 'agent_invalid_response',
21
21
  'tool_not_loaded', 'reconciliation_required', 'unknown_failure',
22
+ 'knowledge_unavailable', 'knowledge_reference_invalid', 'knowledge_reference_expired', 'knowledge_changed', 'knowledge_scope_changed',
22
23
 
23
24
  'agent_invalid_request', 'unauthorized', 'forbidden', 'agent_tool_internal_error',
24
25
  'BAILINGHUB_ACCEPTED_UNKNOWN', 'BAILINGHUB_AGENT_CLIENT_UNAVAILABLE',
@@ -41,6 +42,7 @@ const MESSAGES = {
41
42
  unknown_failure: 'The failure could not be classified. Do not infer that the business capability is absent or repeat an uncertain write.',
42
43
  cancelled: 'This operation or turn has ended. Do not reactivate its tools or create a replacement business call.',
43
44
  invalid_request: 'The request is invalid for this tool. Check the current tool declaration and selected target.',
45
+ knowledge_reference_unavailable: 'The original knowledge reference is invalid, expired or changed. Search knowledge again under the same authorization; never replay a business operation.',
44
46
  }
45
47
 
46
48
  // A generic 404 or error message cannot establish that Core looked up the
@@ -49,6 +51,19 @@ export function isInvocationNotFound(error) {
49
51
  return error?.publicCode === 'invocation_not_found' && error?.statusCode === 404
50
52
  }
51
53
 
54
+ function publicSizeDetails(error, source, code) {
55
+ if (code !== 'arguments_too_large') return {}
56
+ const details = error?.details
57
+ const projected = {}
58
+ const scope = source?.limit_scope ?? details?.limitScope ?? details?.limit_scope
59
+ if (['arguments', 'request_body'].includes(scope)) projected.limit_scope = scope
60
+ for (const [wire, field] of [['actual_bytes', 'actualBytes'], ['max_bytes', 'maxBytes'], ['observed_bytes', 'observedBytes']]) {
61
+ const value = source?.[wire] ?? details?.[field] ?? details?.[wire]
62
+ if (Number.isSafeInteger(value) && value >= 0) projected[wire] = value
63
+ }
64
+ return projected
65
+ }
66
+
52
67
  export function describeFailure(error, context = {}) {
53
68
  const source = error?.feedback?.schema === 'bailing.agent-feedback.v1' ? error.feedback : undefined
54
69
  const operation = context.operation ?? 'tool_dispatch'
@@ -68,7 +83,8 @@ export function describeFailure(error, context = {}) {
68
83
  else if (error?.name === 'AbortError' || code === 'agent_request_cancelled') category = 'cancelled'
69
84
  else if (code === 'tool_not_loaded' || code === 'tool_not_found') { category = 'tool_not_loaded'; next = 'rediscover' }
70
85
  else if (code === 'capability_changed') { category = 'capability_changed'; next = operation === 'resume' ? 'inspect_original' : 'rediscover' }
71
- else if (['agent_transport_unavailable', 'agent_request_timeout'].includes(code)) { category = 'transport_unavailable'; next = operation === 'search' ? 'retry_discovery' : 'restore_scope' }
86
+ else if (['knowledge_reference_invalid', 'knowledge_reference_expired', 'knowledge_changed', 'knowledge_scope_changed'].includes(code)) { category = 'knowledge_reference_unavailable'; next = 'search_knowledge' }
87
+ else if (['agent_transport_unavailable', 'agent_request_timeout', 'knowledge_unavailable'].includes(code)) { category = 'transport_unavailable'; next = operation === 'search' ? 'retry_discovery' : 'restore_scope' }
72
88
  else if (['unauthorized', 'forbidden', 'agent_authorization_unavailable', 'agent_binding_changed', 'AUTHORIZATION_CHANGED', 'route_not_allowed', 'audience_not_allowed', 'conversation_audit_authorization_invalid'].includes(code) || [401, 403].includes(error?.statusCode)) { category = 'authorization_unavailable'; next = 'reauthorize' }
73
89
  else if (['AUTHORIZATION_UNVERIFIED', 'SESSION_SCOPE_UNAVAILABLE'].includes(code)) { category = 'authorization_unavailable'; next = 'restore_scope' }
74
90
  else if (['agent_schema_unsupported', 'CROSS_SYSTEM_SCOPE_UNSUPPORTED', 'CROSS_HUB_SCOPE_UNSUPPORTED'].includes(code)) { category = 'unsupported'; next = 'check_compatibility' }
@@ -106,19 +122,24 @@ export function describeFailure(error, context = {}) {
106
122
  }
107
123
  return {
108
124
  schema: 'bailing.agent-feedback.v1', category, code: missingOriginal ? 'invocation_not_found' : code,
109
- origin: ORIGINS.has(context.origin) ? context.origin : ORIGINS.has(source?.origin) ? source.origin : 'dsh',
125
+ origin: ORIGINS.has(context.origin) ? context.origin : ORIGINS.has(source?.origin) ? source.origin
126
+ : code === 'arguments_too_large' && error?.details?.origin === 'core' ? 'core'
127
+ : code === 'arguments_too_large' && error?.details?.origin === 'client' ? 'sdk' : 'dsh',
110
128
  operation, dispatch,
111
129
  retryable: ['rediscover', 'retry_discovery', 'restore_scope', 'resume_original', 'restore_invocations'].includes(next),
112
130
  next_action: next,
113
131
  ...(invocationId ? { invocation_id: invocationId } : {}),
114
132
  ...(missingOriginal || source?.original_outcome === 'unverified' ? { original_outcome: 'unverified' } : {}),
115
133
  ...(['accepted_unknown', 'definitive_rejection', 'refresh_required'].includes(source?.disposition ?? error?.disposition) ? { disposition: source?.disposition ?? error.disposition } : {}),
134
+ ...publicSizeDetails(error, source, code),
116
135
  message: code.startsWith('TASK_STORE_') ? 'The local task association could not be saved or verified. Restore the original task binding before continuing; do not switch or create a replacement task.' : missingOriginal
117
136
  ? 'The original invocation record was not found. Inspect its original execution evidence; this does not prove that the business action never ran. Do not recreate the call, switch authorization or submit a replacement write.'
118
137
  : MESSAGES[category],
119
138
  }
120
139
  }
121
140
 
141
+ export const BUSINESS_EXECUTION_GUIDANCE = 'Use a loaded tool directly when its current target and declaration are clear. If rate limited before dispatch, wait for retry_after_ms then resume the original invocation_id. Recover pending or uncertain operations only with their original invocation_id; never create a replacement call or repeat a write with a different authorization. inspect_original means stop automatic retries and inspect the original outcome.'
142
+
122
143
  export function normalizeDiscovery(value, returnedCount) {
123
144
  // Missing or malformed optional metadata must not disable an older Core/SDK.
124
145
  const base = {
@@ -0,0 +1,135 @@
1
+ import { createHash, randomBytes } from 'node:crypto'
2
+
3
+ const ID = /^[0-9a-f]{64}$/u
4
+ const REQUEST = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/u
5
+ const MAX_FRAGMENTS = 256
6
+ const MAX_CONFIRM_BYTES = 4 * 1024 * 1024
7
+ const TTL_MS = 60_000
8
+ const digest = value => createHash('sha256').update(value).digest('hex')
9
+ export const contextDigest = value => digest(JSON.stringify(value ?? null))
10
+ const bytes = value => Buffer.byteLength(value, 'utf8')
11
+ const copy = value => structuredClone(value)
12
+ const key = value => `${value.kind}:${value.name}`
13
+ const record = value => value && typeof value === 'object' && !Array.isArray(value)
14
+
15
+ /** Local, presentation-only confirmation. Never grants business permission. */
16
+ export class ContextDeliveryCoordinator {
17
+ constructor({ now = Date.now } = {}) {
18
+ this.now = now
19
+ this.entries = new Map()
20
+ }
21
+
22
+ stage(sessionId, binding, fragments) {
23
+ const previous = this.entries.get(sessionId)
24
+ const generation = (previous?.generation ?? 0) + 1
25
+ if (!binding || !fragments.length || fragments.length > MAX_FRAGMENTS) {
26
+ this.entries.set(sessionId, { generation, reason: 'context_unavailable' })
27
+ return
28
+ }
29
+ const required = fragments.map(fragment => ({ kind: fragment.kind, name: fragment.name,
30
+ sha256: digest(fragment.text), bytes: bytes(fragment.text) }))
31
+ this.entries.set(sessionId, { generation, binding: contextDigest(binding),
32
+ targets: copy(binding.targets), turn: binding.turn, context_revision: binding.context_revision,
33
+ delivery_id: randomBytes(32).toString('hex'), expires_at: this.now() + TTL_MS,
34
+ fragments: required, confirmed: undefined })
35
+ }
36
+
37
+ current(sessionId, binding) {
38
+ const entry = this.entries.get(sessionId)
39
+ if (!entry?.delivery_id) return { reason: entry?.reason ?? 'host_unconfirmed' }
40
+ if (entry.invalidated) return { reason: entry.invalidated }
41
+ if (!binding || entry.binding !== contextDigest(binding)) {
42
+ entry.confirmed = undefined; entry.invalidated = 'binding_changed'
43
+ return { reason: 'binding_changed' }
44
+ }
45
+ if (entry.expires_at <= this.now()) {
46
+ entry.confirmed = undefined; entry.invalidated = 'expired'
47
+ return { reason: 'expired' }
48
+ }
49
+ return { entry }
50
+ }
51
+
52
+ manifest(sessionId, binding) {
53
+ const { entry, reason } = this.current(sessionId, binding)
54
+ if (!entry) return { schema: 'bailing.agent-context-delivery-manifest.v1', state: 'unavailable', reason }
55
+ return { schema: 'bailing.agent-context-delivery-manifest.v1', state: 'available', mode: 'named_runtime',
56
+ delivery_id: entry.delivery_id, generation: entry.generation, turn: entry.turn,
57
+ context_revision: entry.context_revision, expires_at: entry.expires_at,
58
+ targets: copy(entry.targets), fragments: copy(entry.fragments),
59
+ confirmation_scope: 'one_final_provider_request', business_permission: false }
60
+ }
61
+
62
+ reject(reason, generation) {
63
+ return { schema: 'bailing.agent-context-delivery-confirmation.v1', state: 'rejected', reason,
64
+ ...(generation === undefined ? {} : { generation }),
65
+ next_action: 'reassemble_context_before_model_request', business_permission: false }
66
+ }
67
+
68
+ confirm(sessionId, binding, input) {
69
+ const { entry, reason } = this.current(sessionId, binding)
70
+ if (!entry) return this.reject(reason)
71
+ // A late confirmation cannot revoke or replace a newer request's receipt.
72
+ if (!record(input) || input.delivery_id !== entry.delivery_id || input.generation !== entry.generation) {
73
+ return this.reject('stale_confirmation', entry.generation)
74
+ }
75
+ const fail = code => { entry.confirmed = undefined; entry.invalidated = code; return this.reject(code, entry.generation) }
76
+ if (Object.keys(input).some(name => !['delivery_id', 'generation', 'request_id', 'fragments'].includes(name)) ||
77
+ !ID.test(input.delivery_id) || typeof input.request_id !== 'string' || !REQUEST.test(input.request_id) || !Array.isArray(input.fragments) ||
78
+ input.fragments.length !== entry.fragments.length || input.fragments.length > MAX_FRAGMENTS) return fail('invalid_confirmation')
79
+ if (entry.confirmed && entry.confirmed.request_ref !== digest(input.request_id)) return fail('request_generation_reused')
80
+ const actual = new Map()
81
+ let total = 0
82
+ for (const fragment of input.fragments) {
83
+ if (!record(fragment) || Object.keys(fragment).some(name => !['kind', 'name', 'text'].includes(name)) ||
84
+ !['system', 'runtime_context'].includes(fragment.kind) || typeof fragment.name !== 'string' || fragment.name.length > 256 ||
85
+ typeof fragment.text !== 'string' || actual.has(key(fragment))) return fail('invalid_fragment')
86
+ total += bytes(fragment.text)
87
+ if (total > MAX_CONFIRM_BYTES) return fail('confirmation_too_large')
88
+ actual.set(key(fragment), { sha256: digest(fragment.text), bytes: bytes(fragment.text) })
89
+ }
90
+ for (const expected of entry.fragments) {
91
+ const fragment = actual.get(key(expected))
92
+ if (!fragment || fragment.sha256 !== expected.sha256 || fragment.bytes !== expected.bytes) return fail('fragment_mismatch')
93
+ }
94
+ entry.confirmed = { request_ref: digest(input.request_id), confirmed_at: this.now() }
95
+ return { schema: 'bailing.agent-context-delivery-confirmation.v1', state: 'confirmed',
96
+ delivery_id: entry.delivery_id, generation: entry.generation, request_ref: entry.confirmed.request_ref,
97
+ context_revision: entry.context_revision, expires_at: entry.expires_at,
98
+ next_request_requires_confirmation: true, business_permission: false }
99
+ }
100
+
101
+ invalidate(sessionId, reason = 'host_reset') {
102
+ const allowed = ['host_reset', 'request_failed', 'assembly_discarded', 'authorization_unavailable']
103
+ const entry = this.entries.get(sessionId)
104
+ this.entries.set(sessionId, { generation: (entry?.generation ?? 0) + 1,
105
+ reason: allowed.includes(reason) ? reason : 'host_reset' })
106
+ return { schema: 'bailing.agent-context-delivery-reset.v1', state: 'inline', business_permission: false }
107
+ }
108
+
109
+ invalidateConfirmation(sessionId, input, reason) {
110
+ const entry = this.entries.get(sessionId)
111
+ if (entry?.delivery_id === input?.delivery_id && entry?.generation === input?.generation) this.invalidate(sessionId, reason)
112
+ }
113
+
114
+ projection(sessionId, binding, contexts) {
115
+ const { entry, reason } = this.current(sessionId, binding)
116
+ const compact = Boolean(entry?.confirmed)
117
+ return { contexts: compact ? [] : contexts,
118
+ context_delivery: { schema: 'bailing.agent-context-delivery.v1',
119
+ mode: compact ? 'named_runtime' : 'inline', state: compact ? 'confirmed' : 'inline_fallback',
120
+ body_omitted: compact, reason: compact ? 'confirmed_named_runtime' : reason ?? 'host_unconfirmed',
121
+ ...(compact ? { delivery_id: entry.delivery_id, generation: entry.generation,
122
+ request_ref: entry.confirmed.request_ref, context_revision: entry.context_revision,
123
+ references: copy(entry.targets), next_request_requires_confirmation: true } : {}),
124
+ business_permission: false } }
125
+ }
126
+
127
+ forget(sessionId) { this.entries.delete(sessionId) }
128
+ }
129
+
130
+ export function preparationByteMetrics(contexts, returned, schemas, guidance) {
131
+ const source = bytes(JSON.stringify(contexts))
132
+ const inline = bytes(JSON.stringify(returned))
133
+ return { source_context_bytes: source, inline_context_bytes: inline, omitted_context_bytes: source - inline,
134
+ tool_schema_bytes: bytes(JSON.stringify(schemas)), usage_guidance_bytes: guidance === undefined ? 0 : bytes(JSON.stringify(guidance)) }
135
+ }
@@ -0,0 +1,45 @@
1
+ // Model memory is a bounded account of last-known execution facts, not a copy
2
+ // of business output and not proof of a current outcome or complete audit.
3
+ export const EXECUTION_SUMMARY_SCHEMA = 'bailing.authorization-execution-summary.v1'
4
+ const SUMMARY_MAX_CHARS = 16_000
5
+ const INVOCATION_ID = /^[0-9a-f]{64}$/
6
+ const TOOL_NAME = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/
7
+ const STATES = new Set(['executed', 'business_rejected', 'awaiting_approval', 'denied',
8
+ 'rejected_before_dispatch', 'reconciliation_required', 'in_progress', 'outcome_unknown', 'not_dispatched'])
9
+ const UNRESOLVED = new Set(['awaiting_approval', 'in_progress', 'reconciliation_required', 'outcome_unknown'])
10
+
11
+ export function authorizationExecutionSummary(records) {
12
+ const all = [...records].filter(Boolean)
13
+ const facts = all.flatMap((record) => {
14
+ if (!INVOCATION_ID.test(record.invocationId ?? '')) return []
15
+ const state = STATES.has(record.latest?.state)
16
+ ? record.latest.state : record.invoked ? 'outcome_unknown' : 'not_dispatched'
17
+ return [{
18
+ invocation_id: record.invocationId,
19
+ ...(TOOL_NAME.test(record.tool ?? '') ? { tool: record.tool } : {}),
20
+ last_known_state: state,
21
+ ...(UNRESOLVED.has(state) ? { next_action: 'inspect_original' } : {}),
22
+ }]
23
+ })
24
+ // Keep unresolved originals first; a large batch of successful reads must not
25
+ // push an earlier uncertain write out of this bounded continuation context.
26
+ const ordered = [...facts].reverse().sort((a, b) => Number(UNRESOLVED.has(b.last_known_state)) - Number(UNRESOLVED.has(a.last_known_state)))
27
+ const summary = {
28
+ schema_version: EXECUTION_SUMMARY_SCHEMA,
29
+ source: 'agent_client',
30
+ trust: 'reference_only',
31
+ total_invocations: all.length,
32
+ omitted_invocations: all.length,
33
+ invocations: [],
34
+ }
35
+ for (const fact of ordered) {
36
+ summary.invocations.push(fact)
37
+ summary.omitted_invocations = all.length - summary.invocations.length
38
+ if (JSON.stringify(summary).length > SUMMARY_MAX_CHARS) {
39
+ summary.invocations.pop()
40
+ summary.omitted_invocations = all.length - summary.invocations.length
41
+ break
42
+ }
43
+ }
44
+ return JSON.stringify(summary)
45
+ }
package/lib/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import Schema from '@deepseek-ai/schemastery'
2
2
 
3
3
  import { BailingHubAgentClientRuntime } from './runtime.js'
4
+ import { createUsageModelTransport } from './usage-model-transport.js'
4
5
  import { createTransportProvider } from './transport.js'
5
6
 
6
7
  export const name = 'dsh-bailinghub'
@@ -36,6 +37,8 @@ export function createAgentClientPlugin(options = {}) {
36
37
  { scopeStore: options.scopeStore, archiveStore: options.archiveStore, artifactSource: options.artifactSource, artifactStore: options.artifactStore, invocationStore: options.invocationStore, taskStore: options.taskStore, toolLifecycle: options.toolLifecycle },
37
38
  ).install()
38
39
  ctx.provide('bailingHubAgentClient', runtime)
40
+ // Explicit host integration only; installing business tools does not intercept model traffic.
41
+ if (options.usage) ctx.provide('bailingHubUsage', createUsageModelTransport(options.usage))
39
42
  },
40
43
  }
41
44
  }
@@ -60,3 +63,9 @@ export { createFileArtifactStore, createMemoryArtifactStore } from './artifact-s
60
63
  export { createFileInvocationStore, createMemoryInvocationStore } from './invocation-store.js'
61
64
 
62
65
  export { createFileSessionTaskStore, createMemorySessionTaskStore } from './session-task-store.js'
66
+
67
+ export { BailingHubUsageModelTransport, createUsageModelTransport } from './usage-model-transport.js'
68
+ export { usageLlmChunks } from './usage-llm-stream.js'
69
+ export { createFileSessionUsageStore, createMemorySessionUsageStore } from './session-usage-store.js'
70
+
71
+ export { usageNativeModelChunks } from './usage-provider-stream.js'
@@ -92,6 +92,11 @@ export class InvocationJournal {
92
92
  return result
93
93
  }
94
94
 
95
+ // Wait only for local work already queued. Do not read, retry or restore records.
96
+ async drain(sessionId) {
97
+ await this.queues.get(sessionId)
98
+ }
99
+
95
100
  normalizeFailure(error, id) {
96
101
  return error?.feedback ? error : recoveryError(error?.code === 'INVOCATION_STORE_CONFLICT'
97
102
  ? 'invocation_store_conflict' : 'invocation_store_unavailable', id)