dsh-bailinghub 0.5.0 → 0.7.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.
- package/CHANGELOG.md +79 -0
- package/PRIVACY.md +16 -5
- package/README.md +21 -3
- package/SECURITY.md +10 -2
- package/docs/AGENT_CLIENT_CONTRACT.md +27 -7
- package/docs/CAPABILITY_FEEDBACK.md +199 -0
- package/docs/COMPATIBILITY.md +19 -6
- package/docs/CROSS_SYSTEM_CONVERSATIONS.md +4 -2
- package/docs/GENERATED_ARTIFACTS.md +77 -0
- package/docs/GETTING_STARTED.md +5 -5
- package/docs/GETTING_STARTED.zh-CN.md +5 -5
- package/docs/INVOCATION_RECOVERY.md +182 -0
- package/docs/MIGRATION_VNEXT.md +8 -0
- package/docs/README.zh-CN.md +37 -3
- package/docs/RELEASE_NOTES_v0.6.0.en.md +39 -0
- package/docs/RELEASE_NOTES_v0.6.0.md +39 -0
- package/docs/RELEASE_NOTES_v0.7.0.en.md +15 -0
- package/docs/RELEASE_NOTES_v0.7.0.md +15 -0
- package/docs/SESSION_TOOL_REUSE.md +213 -0
- package/docs/TASK_CONTROL.md +183 -0
- package/docs/UPGRADE_v0.6.0.en.md +71 -0
- package/docs/UPGRADE_v0.6.0.md +73 -0
- package/docs/UPGRADE_v0.7.0.en.md +15 -0
- package/docs/UPGRADE_v0.7.0.md +15 -0
- package/docs/USAGE_SERVICE.md +96 -0
- package/lib/artifact-store.js +56 -0
- package/lib/artifacts.js +132 -0
- package/lib/capability-feedback.js +138 -0
- package/lib/conversation-outbox.js +24 -0
- package/lib/index.js +16 -1
- package/lib/invocation-journal.js +228 -0
- package/lib/invocation-store.js +275 -0
- package/lib/model-gateway-transport.js +246 -0
- package/lib/runtime.js +951 -146
- package/lib/session-scope.js +47 -5
- package/lib/session-task-store.js +275 -0
- package/lib/session-task.js +237 -0
- package/lib/session-tool-cache.js +168 -0
- package/lib/session-tools.js +240 -0
- package/lib/session-usage-store.js +280 -0
- package/lib/tool-catalog.js +38 -0
- package/lib/transport.js +19 -0
- package/lib/usage-llm-stream.js +157 -0
- package/lib/usage-model-transport.js +206 -0
- package/lib/usage-provider-stream.js +82 -0
- package/package.json +18 -3
|
@@ -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
|
+
This optional module pairs Core 0.9.0, SDK 0.7.0 and DSH 0.7.0. It is new relative to public SDK/DSH 0.6.0 and has no legacy Token-allowance aliases, silent billing fallback, balance conversion or automatic BYOK substitution. Public Core 0.8.0 upgrades add outstanding 063/064 migrations and retain 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.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { constants } from 'node:fs'
|
|
3
|
+
import { lstat, mkdir, open, link, unlink } from 'node:fs/promises'
|
|
4
|
+
import { resolve, join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
const error = () => Object.assign(new Error('Artifact recovery metadata could not be saved or restored.'), { code: 'storage_error' })
|
|
7
|
+
const fileName = id => { if (!/^[a-f0-9]{64}$/.test(id)) throw error(); return `${id}.json` }
|
|
8
|
+
function checked(value, id) {
|
|
9
|
+
const json = JSON.stringify(value)
|
|
10
|
+
if (json.length > 16384 || value?.uploadId !== id || !value.metadata || !value.binding || !value.artifactRef || !value.sessionId) throw error()
|
|
11
|
+
return JSON.parse(json)
|
|
12
|
+
}
|
|
13
|
+
/** Test/ephemeral hosts only. Production hosts must persist the immutable recovery record. */
|
|
14
|
+
export function createMemoryArtifactStore() {
|
|
15
|
+
const records = new Map()
|
|
16
|
+
return { async get(id) { return structuredClone(records.get(id) ?? null) }, async reserve(value) {
|
|
17
|
+
if (!records.has(value.uploadId)) records.set(value.uploadId, checked(value, value.uploadId))
|
|
18
|
+
return structuredClone(records.get(value.uploadId))
|
|
19
|
+
} }
|
|
20
|
+
}
|
|
21
|
+
/** Atomic write-once receipts; no credentials or file bytes. Keep this directory across restarts. */
|
|
22
|
+
export function createFileArtifactStore({ directory } = {}) {
|
|
23
|
+
if (typeof directory !== 'string' || !directory) throw error()
|
|
24
|
+
const root = resolve(directory)
|
|
25
|
+
async function prepare() {
|
|
26
|
+
await mkdir(root, { recursive: true, mode: 0o700 })
|
|
27
|
+
const stat = await lstat(root)
|
|
28
|
+
if (!stat.isDirectory() || (process.platform !== 'win32' && (stat.mode & 0o777) !== 0o700)) throw error()
|
|
29
|
+
}
|
|
30
|
+
async function get(id) {
|
|
31
|
+
try {
|
|
32
|
+
await prepare()
|
|
33
|
+
let handle
|
|
34
|
+
try { handle = await open(join(root, fileName(id)), constants.O_RDONLY | constants.O_NOFOLLOW) }
|
|
35
|
+
catch (e) { if (e.code === 'ENOENT') return null; throw e }
|
|
36
|
+
try {
|
|
37
|
+
const stat = await handle.stat()
|
|
38
|
+
if (!stat.isFile() || stat.size > 16384 || (process.platform !== 'win32' && (stat.mode & 0o777) !== 0o600)) throw error()
|
|
39
|
+
return checked(JSON.parse(await handle.readFile('utf8')), id)
|
|
40
|
+
} finally { await handle.close() }
|
|
41
|
+
} catch { throw error() }
|
|
42
|
+
}
|
|
43
|
+
return { get, async reserve(value) {
|
|
44
|
+
const data = checked(value, value.uploadId)
|
|
45
|
+
let temporary
|
|
46
|
+
try {
|
|
47
|
+
await prepare()
|
|
48
|
+
temporary = join(root, `${randomUUID()}.tmp`)
|
|
49
|
+
const handle = await open(temporary, 'wx', 0o600)
|
|
50
|
+
try { await handle.writeFile(JSON.stringify(data)); await handle.sync() } finally { await handle.close() }
|
|
51
|
+
try { await link(temporary, join(root, fileName(value.uploadId))) } catch (e) { if (e.code !== 'EEXIST') throw e }
|
|
52
|
+
if (process.platform !== 'win32') { const dir = await open(root, 'r'); try { await dir.sync() } finally { await dir.close() } }
|
|
53
|
+
return await get(value.uploadId)
|
|
54
|
+
} catch { throw error() } finally { if (temporary) await unlink(temporary).catch(() => {}) }
|
|
55
|
+
} }
|
|
56
|
+
}
|
package/lib/artifacts.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
const digest = (...parts) => createHash('sha256').update(JSON.stringify(parts)).digest('hex')
|
|
4
|
+
const fail = code => Object.assign(new Error(code), { code })
|
|
5
|
+
const repairSchema = 'bailing.artifact-run-link-repair.v1'
|
|
6
|
+
const withoutRun = metadata => { const { runId, ...rest } = metadata; return rest }
|
|
7
|
+
const receiptMatches = (receipt, metadata) => ['name', 'mime', 'bytes', 'sha256'].every(k => receipt[k] === metadata[k]) &&
|
|
8
|
+
receipt.client_conversation_id === metadata.clientConversationId && receipt.client_turn_id === metadata.clientTurnId && receipt.run_id === metadata.runId
|
|
9
|
+
const descriptor = v => {
|
|
10
|
+
if (!v || typeof v.artifactRef !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(v.artifactRef) || typeof v.name !== 'string' || !v.name || v.name.length > 128 || /[\\/\u0000-\u001f\u007f]/.test(v.name) ||
|
|
11
|
+
!['image/png', 'image/jpeg', 'image/webp'].includes(v.mime) || !Number.isInteger(v.bytes) || v.bytes < 1 || v.bytes > 6291456 || !/^[a-f0-9]{64}$/.test(v.sha256)) throw fail('artifact_invalid_source')
|
|
12
|
+
return { artifactRef: v.artifactRef, name: v.name, mime: v.mime, bytes: v.bytes, sha256: v.sha256 }
|
|
13
|
+
}
|
|
14
|
+
export async function listArtifacts(source, sessionId) {
|
|
15
|
+
if (typeof source?.list !== 'function') throw fail('artifact_source_unsupported')
|
|
16
|
+
const values = await source.list({ sessionId })
|
|
17
|
+
if (!Array.isArray(values) || values.length > 100) throw fail('artifact_invalid_source')
|
|
18
|
+
const result = values.map(descriptor)
|
|
19
|
+
if (new Set(result.map(x => x.artifactRef)).size !== result.length) throw fail('artifact_invalid_source')
|
|
20
|
+
return result
|
|
21
|
+
}
|
|
22
|
+
/** One immutable original target per receipt. This never invokes a business operation. */
|
|
23
|
+
export async function deliverArtifact({ source, store, transport, sessionId, artifactRef, binding, options, metadata, assertActive }) {
|
|
24
|
+
if (!store?.get || !store?.reserve) throw fail('storage_error')
|
|
25
|
+
if (!transport?.uploadArtifact || !transport?.getArtifact) throw fail('artifact_unsupported')
|
|
26
|
+
const uploadId = digest('bailing.artifact.v1', sessionId, options.connectionKey, artifactRef)
|
|
27
|
+
const repairId = digest(repairSchema, uploadId)
|
|
28
|
+
const validateStored = record => {
|
|
29
|
+
if (!record || record.sessionId !== sessionId || record.artifactRef !== artifactRef || record.uploadId !== uploadId || JSON.stringify(record.binding) !== JSON.stringify(binding)) throw fail('artifact_conflict')
|
|
30
|
+
return record
|
|
31
|
+
}
|
|
32
|
+
let record
|
|
33
|
+
try { record = await store.get(uploadId) } catch { throw fail('storage_error') }
|
|
34
|
+
let repair
|
|
35
|
+
const loadRepair = async () => {
|
|
36
|
+
let saved
|
|
37
|
+
try { saved = await store.get(repairId) } catch { throw fail('storage_error') }
|
|
38
|
+
if (saved) {
|
|
39
|
+
if (!record?.metadata.runId || digest(saved) !== digest(repairRecord())) throw fail('storage_error')
|
|
40
|
+
repair = saved
|
|
41
|
+
} else if (repair) throw fail('storage_error')
|
|
42
|
+
return repair
|
|
43
|
+
}
|
|
44
|
+
const repairRecord = () => ({ uploadId: repairId, sessionId, artifactRef, binding,
|
|
45
|
+
metadata: withoutRun(record.metadata),
|
|
46
|
+
runLinkRepair: { schema: repairSchema, originalUploadId: uploadId, originalMetadataHash: digest(record.metadata), reason: 'artifact_run_turn_mismatch' },
|
|
47
|
+
})
|
|
48
|
+
const checkReceipt = async result => {
|
|
49
|
+
await loadRepair()
|
|
50
|
+
const expected = repair?.metadata ?? record.metadata
|
|
51
|
+
if (!['ready', 'pending'].includes(result?.state)) throw fail('artifact_invalid_receipt')
|
|
52
|
+
if (!receiptMatches(result, expected)) {
|
|
53
|
+
// A corrected remote receipt without its local correction is a history
|
|
54
|
+
// gap, not permission to reconstruct the missing recovery record.
|
|
55
|
+
if (!repair && record.metadata.runId && receiptMatches(result, withoutRun(record.metadata))) throw fail('storage_error')
|
|
56
|
+
throw fail('artifact_conflict')
|
|
57
|
+
}
|
|
58
|
+
return result
|
|
59
|
+
}
|
|
60
|
+
await loadRepair()
|
|
61
|
+
if (record) {
|
|
62
|
+
validateStored(record)
|
|
63
|
+
await assertActive()
|
|
64
|
+
try {
|
|
65
|
+
const result = await checkReceipt(await transport.getArtifact(uploadId, options))
|
|
66
|
+
if (result.state === 'ready') {
|
|
67
|
+
await assertActive()
|
|
68
|
+
return { artifact_ref: artifactRef, ...result }
|
|
69
|
+
}
|
|
70
|
+
} catch (e) { if (e.publicCode !== 'artifact_not_found') throw e }
|
|
71
|
+
}
|
|
72
|
+
await assertActive()
|
|
73
|
+
if (typeof source?.read !== 'function') throw fail('artifact_source_unsupported')
|
|
74
|
+
const item = (await listArtifacts(source, sessionId)).find(x => x.artifactRef === artifactRef)
|
|
75
|
+
if (!item) throw fail('artifact_not_found_local')
|
|
76
|
+
if (record && (record.metadata.sha256 !== item.sha256 || record.metadata.name !== item.name || record.metadata.mime !== item.mime || record.metadata.bytes !== item.bytes)) throw fail('artifact_content_changed')
|
|
77
|
+
const raw = await source.read({ sessionId, artifactRef })
|
|
78
|
+
if (!(raw instanceof Uint8Array)) throw fail('artifact_invalid_source')
|
|
79
|
+
if (raw.byteLength !== item.bytes) throw fail('artifact_content_changed')
|
|
80
|
+
const body = Buffer.from(raw)
|
|
81
|
+
if (body.length !== item.bytes || createHash('sha256').update(body).digest('hex') !== item.sha256) throw fail('artifact_content_changed')
|
|
82
|
+
if (!record) {
|
|
83
|
+
try { record = await store.reserve({ uploadId, sessionId, artifactRef, binding, metadata: { ...metadata, name: item.name, mime: item.mime, bytes: item.bytes, sha256: item.sha256 } }) }
|
|
84
|
+
catch { throw fail('storage_error') }
|
|
85
|
+
validateStored(record)
|
|
86
|
+
if (['sha256', 'name', 'mime', 'bytes'].some(key => record.metadata[key] !== item[key])) throw fail('artifact_content_changed')
|
|
87
|
+
}
|
|
88
|
+
await assertActive()
|
|
89
|
+
await loadRepair()
|
|
90
|
+
let result
|
|
91
|
+
try {
|
|
92
|
+
result = await transport.uploadArtifact({ uploadId, body, ...(repair?.metadata ?? record.metadata) }, options)
|
|
93
|
+
} catch (error) {
|
|
94
|
+
// Only Core's explicit same-identity, same-route, same-conversation proof
|
|
95
|
+
// permits detaching a run from another turn. Generic mismatch stays blocked.
|
|
96
|
+
if (error.publicCode !== 'artifact_run_turn_mismatch' || repair || !record.metadata.runId ||
|
|
97
|
+
record.metadata.clientConversationId !== metadata.clientConversationId) throw error
|
|
98
|
+
await assertActive()
|
|
99
|
+
try {
|
|
100
|
+
const existing = await checkReceipt(await transport.getArtifact(uploadId, options))
|
|
101
|
+
if (existing.state === 'ready') {
|
|
102
|
+
await assertActive()
|
|
103
|
+
return { artifact_ref: artifactRef, ...existing }
|
|
104
|
+
}
|
|
105
|
+
throw error
|
|
106
|
+
} catch (readError) { if (readError.publicCode !== 'artifact_not_found') throw readError }
|
|
107
|
+
await assertActive()
|
|
108
|
+
try { await store.reserve(repairRecord()) } catch { throw fail('storage_error') }
|
|
109
|
+
await loadRepair()
|
|
110
|
+
if (!repair) throw fail('storage_error')
|
|
111
|
+
await assertActive()
|
|
112
|
+
// The original upload ID, file, target and upload turn remain unchanged.
|
|
113
|
+
// The immutable original record retains the rejected run for diagnostics.
|
|
114
|
+
result = await transport.uploadArtifact({ uploadId, body, ...repair.metadata }, options)
|
|
115
|
+
}
|
|
116
|
+
await checkReceipt(result)
|
|
117
|
+
await assertActive()
|
|
118
|
+
return { artifact_ref: artifactRef, ...result }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function artifactFailure(error, artifactRef) {
|
|
122
|
+
const candidate = error?.publicCode ?? error?.code
|
|
123
|
+
const known = new Set(['storage_error', 'artifact_unsupported', 'artifact_source_unsupported', 'artifact_invalid_source', 'artifact_not_found_local', 'artifact_content_changed',
|
|
124
|
+
'artifact_conflict', 'artifact_invalid_request', 'artifact_content_mismatch', 'artifact_type_not_allowed', 'artifact_too_large', 'artifact_run_mismatch', 'artifact_run_turn_mismatch',
|
|
125
|
+
'artifact_storage_changed', 'artifact_upload_disabled', 'artifact_storage_unavailable', 'artifact_upload_pending', 'artifact_invalid_receipt',
|
|
126
|
+
'agent_request_timeout', 'agent_transport_unavailable', 'unauthorized', 'route_not_allowed', 'agent_request_cancelled'])
|
|
127
|
+
const code = known.has(candidate) ? candidate : 'artifact_delivery_blocked'
|
|
128
|
+
const pending = ['artifact_upload_pending', 'agent_request_timeout', 'agent_transport_unavailable'].includes(code)
|
|
129
|
+
return { artifact_ref: artifactRef, state: pending ? 'pending' : 'blocked', error: code,
|
|
130
|
+
next_action: pending ? 'retry_same_artifact_and_authorization' : code === 'storage_error' ? 'repair_local_storage' : 'resolve_error',
|
|
131
|
+
business_operation_performed: false }
|
|
132
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// The SDK owns HTTP error classification. This projection also supports older
|
|
2
|
+
// SDKs and custom transports without importing an optional SDK entry point.
|
|
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'])
|
|
6
|
+
const ORIGINS = new Set(['core', 'sdk', 'mcp', 'dsh', 'host'])
|
|
7
|
+
const CODES = new Set([
|
|
8
|
+
'TASK_DISPATCH_UNCERTAIN', 'TASK_NOT_FOUND', 'TASK_RUN_INACTIVE', 'TASK_INVALID_INPUT',
|
|
9
|
+
'TASK_REQUIRED', 'TASK_UNSUPPORTED', 'TASK_SCOPE_BLOCKED', 'TASK_MEMBER_MISMATCH', 'TASK_BINDING_CONFLICT', 'TASK_PAUSED', 'TASK_CANCELLED', 'TASK_EXPIRED', 'TASK_WRITE_BUDGET_EXHAUSTED', 'TASK_CONCURRENCY_EXHAUSTED', 'TASK_TOOL_NOT_ALLOWED', 'TASK_REVISION_CONFLICT', 'TASK_RECORD_INVALID', 'TASK_UNAVAILABLE', 'TASK_STORE_CONFLICT', 'TASK_STORE_UNAVAILABLE',
|
|
10
|
+
'invocation_binding_unavailable', 'invocation_binding_conflict', 'invocation_store_unavailable', 'invocation_store_conflict', 'invocation_store_unsupported',
|
|
11
|
+
'agent_client_disabled', 'agent_direct_disabled', 'agent_runtime_unavailable', 'agent_tools_unavailable',
|
|
12
|
+
'arguments_too_large', 'assistant_message_conflict', 'audience_not_allowed', 'capability_changed',
|
|
13
|
+
'hub_paused', 'invalid_request', 'invalid_route', 'invocation_conflict', 'invocation_not_found',
|
|
14
|
+
'page_context_too_large', 'route_not_allowed', 'route_unavailable', 'run_completion_conflict',
|
|
15
|
+
'run_not_found', 'tool_not_found', 'turn_conflict', 'conversation_audit_conflict',
|
|
16
|
+
'conversation_audit_not_found', 'conversation_audit_not_ready', 'conversation_audit_authorization_invalid',
|
|
17
|
+
'conversation_audit_limit', 'conversation_audit_unavailable', 'conversation_audit_internal_error',
|
|
18
|
+
'conversation_audit_cross_binding_unavailable', 'system_info_unsupported', 'agent_binding_changed',
|
|
19
|
+
'agent_request_cancelled', 'agent_transport_unavailable', 'agent_request_timeout',
|
|
20
|
+
'agent_authorization_unavailable', 'agent_schema_unsupported', 'agent_invalid_response',
|
|
21
|
+
'tool_not_loaded', 'reconciliation_required', 'unknown_failure',
|
|
22
|
+
|
|
23
|
+
'agent_invalid_request', 'unauthorized', 'forbidden', 'agent_tool_internal_error',
|
|
24
|
+
'BAILINGHUB_ACCEPTED_UNKNOWN', 'BAILINGHUB_AGENT_CLIENT_UNAVAILABLE',
|
|
25
|
+
'AUTHORIZATION_CHANGED', 'AUTHORIZATION_UNVERIFIED', 'SESSION_SCOPE_UNAVAILABLE',
|
|
26
|
+
'CROSS_SYSTEM_SCOPE_UNSUPPORTED', 'CROSS_HUB_SCOPE_UNSUPPORTED',
|
|
27
|
+
'SCOPE_STORE_UNAVAILABLE', 'SCOPE_STORE_CONFLICT', 'SESSION_SCOPE_CONFLICT',
|
|
28
|
+
'INVALID_SCOPE_SNAPSHOT', 'INVALID_SESSION_ID', 'SESSION_LOCAL_STORAGE_ERROR', 'SESSION_RECOVERY_GAP',
|
|
29
|
+
])
|
|
30
|
+
|
|
31
|
+
const MESSAGES = {
|
|
32
|
+
task_control: 'The original task does not currently permit this operation. Inspect its task state; do not rediscover tools, change authorization, replace the invocation or create a new task to bypass the limit.',
|
|
33
|
+
storage_error: 'The local invocation recovery record could not be saved or verified. Restore invocation storage before continuing; do not create a replacement write. This does not confirm the original business outcome.',
|
|
34
|
+
invocation_recovery_unavailable: 'The original invocation binding is missing or conflicts with this fixed session scope. Inspect the original record; never switch target, guess arguments or create a replacement write.',
|
|
35
|
+
tool_not_loaded: 'This business tool is no longer loaded. Discover capabilities for the same selected target; an existing invocation must be recovered using its original ID.',
|
|
36
|
+
capability_changed: 'The capability declaration or its authorization changed. Keep any original invocation and its target; never create a replacement write to recover an uncertain result.',
|
|
37
|
+
transport_unavailable: 'The connection is temporarily unavailable. Retry only the indicated discovery, identity check or original invocation recovery.',
|
|
38
|
+
authorization_unavailable: 'The original authorization cannot currently be used. Do not switch to a default, different authorization or a surviving subset.',
|
|
39
|
+
unsupported: 'The current component does not support this capability. Check compatibility; this does not mean the business system has no capabilities.',
|
|
40
|
+
invocation_outcome_unknown: 'The original operation outcome is unconfirmed. Recover only the original invocation_id; never create a replacement business call.',
|
|
41
|
+
unknown_failure: 'The failure could not be classified. Do not infer that the business capability is absent or repeat an uncertain write.',
|
|
42
|
+
cancelled: 'This operation or turn has ended. Do not reactivate its tools or create a replacement business call.',
|
|
43
|
+
invalid_request: 'The request is invalid for this tool. Check the current tool declaration and selected target.',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// A generic 404 or error message cannot establish that Core looked up the
|
|
47
|
+
// original invocation. Require the SDK's structured public code and HTTP status.
|
|
48
|
+
export function isInvocationNotFound(error) {
|
|
49
|
+
return error?.publicCode === 'invocation_not_found' && error?.statusCode === 404
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function describeFailure(error, context = {}) {
|
|
53
|
+
const source = error?.feedback?.schema === 'bailing.agent-feedback.v1' ? error.feedback : undefined
|
|
54
|
+
const operation = context.operation ?? 'tool_dispatch'
|
|
55
|
+
const id = context.invocationId ?? source?.invocation_id ?? error?.invocationId
|
|
56
|
+
const invocationId = typeof id === 'string' && ID.test(id) ? id : undefined
|
|
57
|
+
const rawCode = source?.code ?? error?.publicCode ?? error?.code
|
|
58
|
+
const code = CODES.has(rawCode) ? rawCode : 'unknown_failure'
|
|
59
|
+
const missingOriginal = (['resume', 'inspect'].includes(operation) && isInvocationNotFound(error)) ||
|
|
60
|
+
(source?.code === 'invocation_not_found' && source?.next_action === 'inspect_original' && source?.original_outcome === 'unverified')
|
|
61
|
+
let category = CATEGORIES.has(source?.category) ? source.category : 'unknown_failure'
|
|
62
|
+
let next = ACTIONS.has(source?.next_action) ? source.next_action : 'none'
|
|
63
|
+
let dispatch = ['not_dispatched', 'attempted', 'unknown'].includes(source?.dispatch)
|
|
64
|
+
? source.dispatch : context.dispatch ?? (['invoke', 'resume'].includes(operation) ? 'unknown' : 'not_dispatched')
|
|
65
|
+
if (!source) {
|
|
66
|
+
if (['TASK_DISPATCH_UNCERTAIN', 'TASK_UNAVAILABLE'].includes(code) && ['invoke', 'resume'].includes(operation)) { category = 'invocation_outcome_unknown'; next = 'inspect_original'; dispatch = 'unknown' }
|
|
67
|
+
else if (code.startsWith('TASK_')) { category = code.startsWith('TASK_STORE_') ? 'storage_error' : code === 'TASK_UNSUPPORTED' ? 'unsupported' : 'task_control'; next = category === 'storage_error' ? 'restore_task' : category === 'unsupported' ? 'check_compatibility' : 'inspect_task'; dispatch = 'not_dispatched' }
|
|
68
|
+
else if (error?.name === 'AbortError' || code === 'agent_request_cancelled') category = 'cancelled'
|
|
69
|
+
else if (code === 'tool_not_loaded' || code === 'tool_not_found') { category = 'tool_not_loaded'; next = 'rediscover' }
|
|
70
|
+
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' }
|
|
72
|
+
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
|
+
else if (['AUTHORIZATION_UNVERIFIED', 'SESSION_SCOPE_UNAVAILABLE'].includes(code)) { category = 'authorization_unavailable'; next = 'restore_scope' }
|
|
74
|
+
else if (['agent_schema_unsupported', 'CROSS_SYSTEM_SCOPE_UNSUPPORTED', 'CROSS_HUB_SCOPE_UNSUPPORTED'].includes(code)) { category = 'unsupported'; next = 'check_compatibility' }
|
|
75
|
+
else if (['agent_invalid_request', 'invalid_request', 'invalid_route', 'arguments_too_large', 'page_context_too_large'].includes(code) || error instanceof TypeError && !['invoke', 'resume'].includes(operation)) { category = 'invalid_request'; dispatch = 'not_dispatched' }
|
|
76
|
+
}
|
|
77
|
+
if (!source && operation === 'invoke' && ['definitive_rejection', 'refresh_required'].includes(error?.disposition) &&
|
|
78
|
+
([401, 403].includes(error?.statusCode) || ['capability_changed', 'tool_not_found', 'invalid_request', 'route_not_allowed', 'audience_not_allowed'].includes(code))) {
|
|
79
|
+
dispatch = 'not_dispatched'
|
|
80
|
+
}
|
|
81
|
+
// A previously dispatched call takes precedence over tool discovery advice.
|
|
82
|
+
if (error?.disposition === 'accepted_unknown' || source?.disposition === 'accepted_unknown' || category === 'invocation_outcome_unknown') {
|
|
83
|
+
category = 'invocation_outcome_unknown'; dispatch = dispatch === 'not_dispatched' ? 'unknown' : dispatch; next = source?.next_action === 'inspect_original' || ['reconciliation_required', 'TASK_DISPATCH_UNCERTAIN', 'TASK_UNAVAILABLE'].includes(code) || !invocationId ? 'inspect_original' : 'resume_original'
|
|
84
|
+
} else if (!source && operation === 'resume' && invocationId && !['cancelled', 'authorization_unavailable', 'unsupported', 'task_control', 'storage_error'].includes(category)) {
|
|
85
|
+
next = ['transport_unavailable', 'unknown_failure'].includes(category) ? 'resume_original' : 'inspect_original'
|
|
86
|
+
} else if (!source && operation === 'invoke' && invocationId && dispatch !== 'not_dispatched' && !['cancelled', 'authorization_unavailable', 'unsupported', 'capability_changed', 'task_control', 'storage_error'].includes(category)) {
|
|
87
|
+
category = 'invocation_outcome_unknown'; next = 'resume_original'
|
|
88
|
+
}
|
|
89
|
+
if (missingOriginal) {
|
|
90
|
+
// This is evidence about recovery availability, not proof that the original
|
|
91
|
+
// business action never ran. Preserve it through the host's second projection.
|
|
92
|
+
category = 'invocation_outcome_unknown'
|
|
93
|
+
dispatch = dispatch === 'attempted' ? 'attempted' : 'unknown'
|
|
94
|
+
next = 'inspect_original'
|
|
95
|
+
}
|
|
96
|
+
if (operation === 'task_coordinates') {
|
|
97
|
+
// Coordinates never dispatch. Preserve the shared feedback envelope while
|
|
98
|
+
// distinguishing local persistence from retryable read-only network probes.
|
|
99
|
+
dispatch = 'not_dispatched'
|
|
100
|
+
if (['SCOPE_STORE_UNAVAILABLE', 'SCOPE_STORE_CONFLICT'].includes(code)) { category = 'storage_error'; next = 'restore_scope' }
|
|
101
|
+
else if (['SESSION_LOCAL_STORAGE_ERROR', 'SESSION_RECOVERY_GAP'].includes(code)) { category = 'storage_error'; next = 'none' }
|
|
102
|
+
else if (['SESSION_SCOPE_CONFLICT', 'INVALID_SCOPE_SNAPSHOT'].includes(code)) { category = 'authorization_unavailable'; next = 'restore_scope' }
|
|
103
|
+
else if (code === 'INVALID_SESSION_ID') { category = 'invalid_request'; next = 'none' }
|
|
104
|
+
else if (['TASK_UNAVAILABLE', 'agent_runtime_unavailable', 'agent_invalid_response'].includes(code)) { category = 'transport_unavailable'; next = 'restore_scope' }
|
|
105
|
+
else if (error?.statusCode === 429 || error?.statusCode >= 500) { category = 'transport_unavailable'; next = 'restore_scope' }
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
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',
|
|
110
|
+
operation, dispatch,
|
|
111
|
+
retryable: ['rediscover', 'retry_discovery', 'restore_scope', 'resume_original', 'restore_invocations'].includes(next),
|
|
112
|
+
next_action: next,
|
|
113
|
+
...(invocationId ? { invocation_id: invocationId } : {}),
|
|
114
|
+
...(missingOriginal || source?.original_outcome === 'unverified' ? { original_outcome: 'unverified' } : {}),
|
|
115
|
+
...(['accepted_unknown', 'definitive_rejection', 'refresh_required'].includes(source?.disposition ?? error?.disposition) ? { disposition: source?.disposition ?? error.disposition } : {}),
|
|
116
|
+
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
|
+
? '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
|
+
: MESSAGES[category],
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function normalizeDiscovery(value, returnedCount) {
|
|
123
|
+
// Missing or malformed optional metadata must not disable an older Core/SDK.
|
|
124
|
+
const base = {
|
|
125
|
+
mode: 'unknown', scope: 'current_authorization', returned_count: returnedCount,
|
|
126
|
+
authorized_total: null, matched_total: null, matched_total_exact: false,
|
|
127
|
+
limit: null, truncated: null, has_more: null, truncation_scope: 'unknown', pagination: 'unsupported',
|
|
128
|
+
}
|
|
129
|
+
if (!value || value.mode !== 'ranked_candidates' || value.scope !== 'current_authorization' ||
|
|
130
|
+
value.returned_count !== returnedCount || !Number.isSafeInteger(value.authorized_total) || value.authorized_total < returnedCount ||
|
|
131
|
+
!Number.isSafeInteger(value.limit) || value.limit < 1 || value.limit > 12 || returnedCount > value.limit ||
|
|
132
|
+
value.matched_total !== null || value.matched_total_exact !== false || value.truncation_scope !== 'authorized_catalog' || value.pagination !== 'unsupported' ||
|
|
133
|
+
value.truncated !== (value.authorized_total > returnedCount) || value.has_more !== value.truncated) return base
|
|
134
|
+
return { ...base, mode: 'ranked_candidates', authorized_total: value.authorized_total, limit: value.limit,
|
|
135
|
+
truncated: value.truncated, has_more: value.has_more, truncation_scope: 'authorized_catalog' }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const DISCOVERY_GUIDANCE = 'Capability search returns bounded ranked candidates, not an exhaustive matching list or pageable catalog. Search adds candidates to the current turn while the authorization catalog revision is unchanged; a changed revision resets that target’s cache. active_tools lists retained, directly callable tools (up to 64 shared); visible_tools is the smaller full-schema window (up to 12). Tools outside that window remain callable using their known exact schema and target. Only eviction, changed declarations or an ended turn require rediscovery. Use currently loaded, valid tools directly when the selected target is clear; search again only when needed. not_loaded means not yet loaded, not no capability. Never use discovery to replace an uncertain invocation.'
|
|
@@ -117,6 +117,30 @@ export class ConversationOutbox {
|
|
|
117
117
|
})
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
// Host receipt actions may reopen before a model turn. Read only the original
|
|
121
|
+
// local outbox; do not create an archive, reconstruct text, flush or upload it.
|
|
122
|
+
restoreExisting(sessionId, value) {
|
|
123
|
+
const context = contextSnapshot(value)
|
|
124
|
+
let entry = this.entries.get(sessionId)
|
|
125
|
+
if (entry && digest(entry.context) !== digest(context)) throw fail('ARCHIVE_BINDING_CHANGED')
|
|
126
|
+
if (!entry) {
|
|
127
|
+
entry = { sessionId, context, pending: [], status: 'inactive' }
|
|
128
|
+
this.entries.set(sessionId, entry)
|
|
129
|
+
}
|
|
130
|
+
return this.serialize(entry, async () => {
|
|
131
|
+
try {
|
|
132
|
+
if (entry.status === 'storage_error') return this.status(sessionId)
|
|
133
|
+
if (!entry.record) {
|
|
134
|
+
const saved = await this.store.load(sessionId)
|
|
135
|
+
if (saved) entry.record = validateRecord(saved, sessionId, context)
|
|
136
|
+
}
|
|
137
|
+
entry.coverageLoaded = true
|
|
138
|
+
entry.status = entry.pending.length ? 'storage_error' : entry.record?.syncState ?? 'inactive'
|
|
139
|
+
return this.status(sessionId)
|
|
140
|
+
} catch (error) { entry.status = 'storage_error'; throw error }
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
120
144
|
async save(entry, values) {
|
|
121
145
|
const previous = entry.record
|
|
122
146
|
const record = validateRecord({ ...previous, ...values, revision: previous.revision + 1 }, entry.sessionId, entry.context)
|
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'
|
|
@@ -33,9 +34,11 @@ export function createAgentClientPlugin(options = {}) {
|
|
|
33
34
|
config,
|
|
34
35
|
getTransport,
|
|
35
36
|
options.recovery,
|
|
36
|
-
{ scopeStore: options.scopeStore, archiveStore: options.archiveStore },
|
|
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
|
}
|
|
@@ -54,3 +57,15 @@ export {
|
|
|
54
57
|
createLazySdkTransport,
|
|
55
58
|
createTransportProvider,
|
|
56
59
|
} from './transport.js'
|
|
60
|
+
|
|
61
|
+
export { createFileArtifactStore, createMemoryArtifactStore } from './artifact-store.js'
|
|
62
|
+
|
|
63
|
+
export { createFileInvocationStore, createMemoryInvocationStore } from './invocation-store.js'
|
|
64
|
+
|
|
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'
|