agentworkshop 0.7.47 → 0.7.49
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/.output/nitro.json +1 -1
- package/.output/public/_nuxt/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/0530cee1-fff5-4025-a6f4-5708b6534608.json +1 -0
- package/.output/server/chunks/_/nitro.mjs +871 -857
- package/.output/server/package.json +1 -1
- package/README-zh.md +813 -806
- package/README.md +823 -816
- package/config.yml +176 -176
- package/docs/aml.md +1 -0
- package/docs/cli.en.md +445 -445
- package/docs/cli.md +410 -410
- package/docs/full-test-plan.md +191 -191
- package/docs/plugins.en.md +567 -567
- package/docs/plugins.md +560 -560
- package/docs/sdk.en.md +922 -922
- package/docs/sdk.md +861 -861
- package/package.json +172 -172
- package/scripts/_aw0746-stage12.mjs +4 -4
- package/scripts/_aw0746-stage34.mjs +6 -4
- package/scripts/_aw0746-stage5.mjs +159 -106
- package/scripts/_aw0746-stage6-aml.mjs +3 -3
- package/scripts/_aw0746-stage6b-train-sweep.mjs +6 -3
- package/scripts/_aw0746-stage6c-jobsmoke.mjs +69 -0
- package/scripts/_aw0746-stage7-twin.mjs +301 -259
- package/scripts/_aw0747-readme-rows.mjs +32 -0
- package/scripts/_aw0748-failclosed-probe.mjs +117 -0
- package/scripts/_aw0748-fresh-channel.mjs +21 -0
- package/scripts/_aw0748-mock-closedloop.mjs +89 -0
- package/scripts/_aw0748-wake-channel.mjs +46 -0
- package/scripts/_aw0749-readme-rows.mjs +23 -0
- package/scripts/_probe-lease.cjs +13 -0
- package/scripts/_probe-task-queue.cjs +9 -0
- package/server/services/workshop/aml/twin/physics-runtime.ts +16 -1
- package/server/services/workshop/aml/twin/provider-contracts.ts +34 -0
- package/server/services/workshop/aml/twin/provider-health.ts +8 -0
- package/server/services/workshop/aml/twin/provider-loader.ts +67 -0
- package/server/services/workshop/aml/twin/provider-registry.ts +50 -0
- package/server/services/workshop/runtime/manager/state.ts +21 -4
- package/.output/public/_nuxt/builds/meta/1b18aaf2-ccbc-45e0-b9c3-107655b226f9.json +0 -1
- package/scripts/_probe-density.cjs +0 -15
- package/scripts/_probe-gate-model.mjs +0 -34
- package/scripts/_probe-model-eligibility.cjs +0 -13
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 临时:把 0.7.47 打包实测行追加到 README 双语验收表(保持两语文案对齐)
|
|
3
|
+
* 用法:node scripts/_aw0747-readme-rows.mjs
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
6
|
+
|
|
7
|
+
const zhRows = [
|
|
8
|
+
'| `_aw0746-stage12.mjs` | **10 / 0**(2026-09-25,全局安装的 **v0.7.47 打包系统**) | 健康门(version=0.7.47)→ 管理员登录/角色面 → 创建 Channel(真实 `omp` lead)→ 开群聊 → **真实 omp 群聊回复** → 系统监控可见 omp 子进程 | `AW_BASE=<base> node scripts/_aw0746-stage12.mjs` |',
|
|
9
|
+
'| `_aw0746-stage34.mjs` | **11 / 0**(2026-09-25,v0.7.47 打包系统 + PLC 模拟器) | 模拟器建线(11 数控 + 14 数采)→ 产品/配方三元组 → 开跑后**样本逐条带 recipe_id/run_id** → `daq_query` **默认只取当前活动批次**(`scope=all` 可放开,实测 1 vs 4)→ 实例化 Hybrid Twin 模板(lead + 3 worker,注入孪生 6 件套 / 总 57 工具) | `AW_BASE=<base> node scripts/_aw0746-stage34.mjs` |',
|
|
10
|
+
'| `_aw0746-stage6b-train-sweep.mjs` | 门禁**全绿**(2026-09-25,v0.7.46 打包系统,真实 torch 训练) | 1 s 拍格数据集(**1724 行 / 10 批次**)→ 内联 code 作业真实训练 → G1 单步 **0.0992**(≤0.10)/ G2 滚动 0.1092(≤0.25)/ G3 泛化 14.6%(≤20%)/ G4 行数与批次 → 模型注册并**晋升 production** | `AW_BASE=<base> AW_DATASET_ID=<ds> node scripts/_aw0746-stage6b-train-sweep.mjs` |',
|
|
11
|
+
'| `_aw0746-stage7-twin.mjs` | **17 / 0**(2026-09-25,v0.7.47 打包系统,真实 DAQ + 真实模型) | 真实样本 → TwinSnapshot `fresh=true` → 65 组真实 VirtualTrial(12 条安全候选,`candidateExecuted=false`)→ 12 项推荐门禁 **write_eligible** → 门禁结论回写模型 `twinEligibility` → `mpc_optimize` 升档 **precise_search** → 全程 **0 DCW 写入** | `AW_BASE=<base> AW_REQUIRE_MODEL=1 node scripts/_aw0746-stage7-twin.mjs` |',
|
|
12
|
+
'| `_aw0746-stage6c-jobsmoke.mjs` | **5 / 0**(2026-09-25,v0.7.47 打包系统) | AML 作业链路:内联 `code` 被接受(不再要求 `workspace/train.py`)→ 作业到终态 → 训练日志/ONNX 痕迹 → 实验与逐项门禁落库 | `AW_BASE=<base> node scripts/_aw0746-stage6c-jobsmoke.mjs` |',
|
|
13
|
+
]
|
|
14
|
+
const enRows = [
|
|
15
|
+
'| `_aw0746-stage12.mjs` | **10 / 0** (2026-09-25, globally installed **v0.7.47 packaged build**) | health gate (version=0.7.47) → admin login/role surface → channel creation with a real `omp` lead → group chat opened → **real omp reply** → omp child process visible in monitoring | `AW_BASE=<base> node scripts/_aw0746-stage12.mjs` |',
|
|
16
|
+
'| `_aw0746-stage34.mjs` | **11 / 0** (2026-09-25, v0.7.47 packaged build + PLC simulator) | line provisioned from the simulator (11 DCW + 14 DAQ) → product/recipe triple → **every sample tagged with recipe_id/run_id** while running → `daq_query` **defaults to the active batch** (`scope=all` opts out, measured 1 vs 4) → Hybrid Twin template instantiated (lead + 3 workers, 6 twin tools injected, 57 total) | `AW_BASE=<base> node scripts/_aw0746-stage34.mjs` |',
|
|
17
|
+
'| `_aw0746-stage6b-train-sweep.mjs` | gates **all green** (2026-09-25, v0.7.46 packaged build, real torch training) | 1 s grid dataset (**1724 rows / 10 runs**) → inline-code job trained for real → G1 one-step **0.0992** (≤0.10) / G2 rollout 0.1092 (≤0.25) / G3 generalisation 14.6% (≤20%) / G4 rows+runs → model registered and **promoted to production** | `AW_BASE=<base> AW_DATASET_ID=<ds> node scripts/_aw0746-stage6b-train-sweep.mjs` |',
|
|
18
|
+
'| `_aw0746-stage7-twin.mjs` | **17 / 0** (2026-09-25, v0.7.47 packaged build, real DAQ + real model) | real samples → TwinSnapshot `fresh=true` → 65 real VirtualTrials (12 safe candidates, `candidateExecuted=false`) → 12-check recommendation gate **write_eligible** → verdict written back to the model as `twinEligibility` → `mpc_optimize` upgrades to **precise_search** → **0 DCW writes** throughout | `AW_BASE=<base> AW_REQUIRE_MODEL=1 node scripts/_aw0746-stage7-twin.mjs` |',
|
|
19
|
+
'| `_aw0746-stage6c-jobsmoke.mjs` | **5 / 0** (2026-09-25, v0.7.47 packaged build) | AML job path: inline `code` accepted (no `workspace/train.py` required) → job reaches a terminal state → training logs/ONNX traces → experiment plus per-check gates persisted | `AW_BASE=<base> node scripts/_aw0746-stage6c-jobsmoke.mjs` |',
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
for (const [file, anchor, rows] of [
|
|
23
|
+
['README-zh.md', '| 前端验收 |', zhRows],
|
|
24
|
+
['README.md', '| Front-end acceptance |', enRows],
|
|
25
|
+
]) {
|
|
26
|
+
const lines = readFileSync(file, 'utf8').split('\n')
|
|
27
|
+
const i = lines.findIndex(l => l.startsWith(anchor))
|
|
28
|
+
if (i < 0) throw new Error(`anchor not found in ${file}`)
|
|
29
|
+
lines.splice(i + 1, 0, ...rows)
|
|
30
|
+
writeFileSync(file, lines.join('\n'), 'utf8')
|
|
31
|
+
console.log(`inserted ${rows.length} rows after line ${i + 1} in ${file}`)
|
|
32
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.7.48 打包系统验收 · 硬约束失败关闭探针(无需 LLM,秒级)
|
|
3
|
+
* 场景:同一份真实快照 + 同一组候选,只改 constraints[].id
|
|
4
|
+
* ① id = weight(可映射)→ 试验正常给出约束结论
|
|
5
|
+
* ② id = part_weight(不可映射)→ 必须**判不通过**并给出"无法映射"明细(v0.7.47 会假通过)
|
|
6
|
+
* 用法:AW_BASE=http://127.0.0.1:3001 [AW_HOME=<home>] node scripts/_aw0748-failclosed-probe.mjs
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from 'node:fs'
|
|
9
|
+
|
|
10
|
+
const BASE = process.env.AW_BASE ?? 'http://127.0.0.1:3001'
|
|
11
|
+
const HOME = process.env.AW_HOME ?? `${process.cwd()}/.aw0746-home`
|
|
12
|
+
const ADMIN = { email: process.env.AW_ADMIN_EMAIL ?? 'admin@awshop.local', password: process.env.AW_ADMIN_PASSWORD ?? 'Awshop@123!' }
|
|
13
|
+
|
|
14
|
+
let pass = 0
|
|
15
|
+
const fails = []
|
|
16
|
+
const check = (name, ok, detail = '') => {
|
|
17
|
+
console.log(` ${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`)
|
|
18
|
+
if (ok) pass += 1
|
|
19
|
+
else fails.push(name)
|
|
20
|
+
}
|
|
21
|
+
const j = async (method, path, body, token) => {
|
|
22
|
+
const r = await fetch(BASE + path, {
|
|
23
|
+
method,
|
|
24
|
+
headers: { 'content-type': 'application/json', ...(token ? { authorization: `Bearer ${token}` } : {}) },
|
|
25
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
26
|
+
signal: AbortSignal.timeout(120_000),
|
|
27
|
+
})
|
|
28
|
+
return { status: r.status, ...(await r.json().catch(() => ({}))) }
|
|
29
|
+
}
|
|
30
|
+
const token = (await j('POST', '/api/users/login', ADMIN)).data?.token
|
|
31
|
+
console.log(`\n═══ 0.7.48 硬约束失败关闭探针 @ ${BASE} ═══`)
|
|
32
|
+
|
|
33
|
+
const chans = (await j('GET', '/api/workshop/channels', undefined, token)).data ?? []
|
|
34
|
+
const twin = chans.find(c => /闭环优化-hybridtwin/.test(c.name))
|
|
35
|
+
const members = (await j('GET', `/api/workshop/channels/${twin.id}/agents`, undefined, token)).data ?? []
|
|
36
|
+
const lead = members.find(m => m.role === 'lead')
|
|
37
|
+
const invoke = async (tool, args) => (await j('POST', '/api/workshop/agent-tools/invoke', { agentId: lead.id, tool, args }, token)).data?.result ?? {}
|
|
38
|
+
const firstJson = (text) => {
|
|
39
|
+
const a = text.indexOf('{')
|
|
40
|
+
const b = text.lastIndexOf('}')
|
|
41
|
+
return a >= 0 && b > a ? JSON.parse(text.slice(a, b + 1)) : null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 快照必须"真新鲜":VirtualTrial 只认 60s 内的水位 → 先开跑采样,再用 auto_daq 让服务端按绑定取样
|
|
45
|
+
const dcw = (await j('GET', '/api/workshop/dcw', undefined, token)).data
|
|
46
|
+
const line = (dcw.lines ?? []).find(l => /injection-aw0746/i.test(String(l.name)))
|
|
47
|
+
const product = (dcw.products ?? []).find(p => p.lineId === line.id)
|
|
48
|
+
const recipe = (dcw.recipes ?? []).find(r => r.lineId === line.id)
|
|
49
|
+
await j('POST', `/api/workshop/dcw/lines/${line.id}/stop`, {}, token)
|
|
50
|
+
await new Promise(r => setTimeout(r, 2500))
|
|
51
|
+
const st = await j('POST', `/api/workshop/dcw/lines/${line.id}/start`, { recipeId: recipe.id }, token)
|
|
52
|
+
console.log(` · 开跑采样:start status=${st.status} ${st.code ?? ''}`)
|
|
53
|
+
await new Promise(r => setTimeout(r, 8000))
|
|
54
|
+
|
|
55
|
+
const daq = (await j('GET', '/api/workshop/daq', undefined, token)).data
|
|
56
|
+
const daqNodes = (daq.nodes ?? []).filter(n => n.lineId === line.id)
|
|
57
|
+
const byName = re => daqNodes.find(n => re.test(n.name))
|
|
58
|
+
const baseScene = firstJson(String((await invoke('twin_scene_read', {})).text ?? ''))
|
|
59
|
+
const scene = {
|
|
60
|
+
...baseScene,
|
|
61
|
+
lineId: line.id,
|
|
62
|
+
productId: product.id,
|
|
63
|
+
recipeId: recipe.id,
|
|
64
|
+
observations: baseScene.observations.map(o => ({ ...o, nodeId: byName({ weight: /part-weight/, flash_rate: /flash-rate/, sink_rate: /sink-mark/ }[o.id])?.id })),
|
|
65
|
+
states: baseScene.states.map(s => ({ ...s, nodeId: byName({ melt_temperature: /melt-temp-pv/, cavity_pressure: /melt-pressure-pv/ }[s.id])?.id })),
|
|
66
|
+
}
|
|
67
|
+
// auto_daq 走 Agent 的真实 DAQ 绑定:探针必须自备绑定(模板实例化不自带)
|
|
68
|
+
const requiredNodeIds = [...scene.observations, ...scene.states].map(o => o.nodeId).filter(Boolean)
|
|
69
|
+
let bound = 0
|
|
70
|
+
for (const m of members) {
|
|
71
|
+
for (const nodeId of requiredNodeIds) {
|
|
72
|
+
const r = await j('POST', '/api/workshop/agent-tools/bindings', { agentId: m.id, nodeId, kind: 'daq', mode: 'auto' }, token)
|
|
73
|
+
if (r.code === 0) bound += 1
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
check('探针自备 DAQ 绑定(auto_daq 前置)', bound >= members.length * requiredNodeIds.length, `bound=${bound} members=${members.length} nodes=${requiredNodeIds.length}`)
|
|
77
|
+
const snapRes = await invoke('twin_snapshot_create', { scene_json: scene, channel_id: twin.id, phase: 'holding', controls: { hold_pressure: 65, hold_time: 8, melt_temperature_setpoint: 247 }, auto_daq: true, freshness_max_ms: 300_000 })
|
|
78
|
+
const snapText = String(snapRes.text ?? '')
|
|
79
|
+
const snapshotId = (snapText.match(/snapshot_id:\s*(\S+)/) ?? [])[1]
|
|
80
|
+
const snapshot = snapshotId ? JSON.parse(readFileSync(`${HOME}/aml/twins/snapshots/${snapshotId}/snapshots.json`, 'utf8')) : null
|
|
81
|
+
check('真实快照生成(auto_daq 按绑定取样)', Boolean(snapshot) && snapshot.dataQuality?.fresh === true && Number(snapshot.daqWatermark) > 0, snapshot ? `${snapshotId} fresh=${snapshot.dataQuality?.fresh} stale=${(snapshot?.dataQuality?.staleNodeIds ?? []).length}` : snapText.slice(0, 160))
|
|
82
|
+
if (!snapshot) {
|
|
83
|
+
console.log(`\n★ 0.7.48 探针:${pass} 通过 / ${fails.length} 失败(无法建快照,后续断言无意义)`)
|
|
84
|
+
process.exit(2)
|
|
85
|
+
}
|
|
86
|
+
const controls = { hold_pressure: 65, hold_time: 8, melt_temperature_setpoint: 247 }
|
|
87
|
+
const objective = { schemaVersion: 1, createdAt: new Date().toISOString(), createdBy: lead.id, objectiveId: 'weight-quality', targets: { weight: 32.5 }, weights: { weight: 1 }, controlCosts: {}, horizonSteps: 1, trustRegion: {} }
|
|
88
|
+
const trialOf = async (scene) => {
|
|
89
|
+
const res = await invoke('twin_trial_run', { scene_json: scene, snapshot_json: snapshot, baseline_controls: controls, candidate_controls: [{ ...controls, hold_pressure: 67 }], objective })
|
|
90
|
+
const text = String(res.text ?? '')
|
|
91
|
+
const id = (text.match(/trial_id:\s*(\S+)/) ?? [])[1]
|
|
92
|
+
const artifact = id ? JSON.parse(readFileSync(`${HOME}/aml/twins/trials/${id}/trials.json`, 'utf8')) : null
|
|
93
|
+
return { text, trial: artifact?.trial ?? null }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ① 可映射 id:baseline 预测克重 32.5 g 落在 31~34 内 → 应通过
|
|
97
|
+
const mapped = { ...scene, constraints: [{ id: 'weight', kind: 'hard_range', min: 31, max: 34 }] }
|
|
98
|
+
const okTrial = await trialOf(mapped)
|
|
99
|
+
const okWeight = okTrial.trial?.constraintResults?.[0]
|
|
100
|
+
check('可映射约束 id(weight)正常评估', Boolean(okTrial.trial) && okWeight?.id === 'weight' && okWeight.passed === true, `weight=${okTrial.trial?.predictedTrajectory?.[0]?.weight?.toFixed?.(3)} detail=${okWeight?.detail}`)
|
|
101
|
+
|
|
102
|
+
// ② 不可映射 id:必须失败关闭(旧版本这里会返回"全轨迹通过")
|
|
103
|
+
const unmapped = { ...mapped, constraints: [{ id: 'part_weight', kind: 'hard_range', min: 31, max: 34 }] }
|
|
104
|
+
const badTrial = await trialOf(unmapped)
|
|
105
|
+
const badCon = badTrial.trial?.constraintResults?.[0]
|
|
106
|
+
const allPassed = (badTrial.trial?.constraintResults ?? []).every(c => c.passed)
|
|
107
|
+
check('不可映射约束 id 判为不通过(失败关闭)', Boolean(badTrial.trial) && badCon?.passed === false && /无法映射/.test(String(badCon?.detail)), `passed=${badCon?.passed} detail=${String(badCon?.detail).slice(0, 80)}`)
|
|
108
|
+
check('候选不再被当作安全(constraints_passed=false)', allPassed === false, `constraints_passed=${allPassed}`)
|
|
109
|
+
check('工具文本同步反映未通过', /constraints_passed:\s*false/.test(badTrial.text), (badTrial.text.match(/constraints_passed:.*/) ?? [''])[0])
|
|
110
|
+
|
|
111
|
+
// ③ 空轨迹(候选为空)同样失败关闭
|
|
112
|
+
const empty = await invoke('twin_trial_run', { scene_json: mapped, snapshot_json: snapshot, baseline_controls: controls, candidate_controls: [], objective })
|
|
113
|
+
const emptyTrialId = (String(empty.text ?? '').match(/trial_id:\s*(\S+)/) ?? [])[1]
|
|
114
|
+
const emptyTrial = emptyTrialId ? JSON.parse(readFileSync(`${HOME}/aml/twins/trials/${emptyTrialId}/trials.json`, 'utf8')).trial : null
|
|
115
|
+
check('空轨迹判为不通过(不再默认通过)', emptyTrial ? emptyTrial.constraintResults.every(c => c.passed === false) : false, `details=${(emptyTrial?.constraintResults ?? []).map(c => c.detail).join(' | ').slice(0, 80)}`)
|
|
116
|
+
|
|
117
|
+
console.log(`\n★ 0.7.48 探针:${pass} 通过 / ${fails.length} 失败${fails.length ? ` (${fails.join('; ')})` : ''}`)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** 临时:实例化全新的 hybrid_twin Channel 用于闭环验收(避开历史队列),输出 channelId */
|
|
2
|
+
const BASE = process.env.AW_BASE ?? 'http://127.0.0.1:3001'
|
|
3
|
+
const ADMIN = { email: process.env.AW_ADMIN_EMAIL ?? 'admin@awshop.local', password: process.env.AW_ADMIN_PASSWORD ?? 'Awshop@123!' }
|
|
4
|
+
const j = async (method, path, body, token) => {
|
|
5
|
+
const r = await fetch(BASE + path, { method, headers: { 'content-type': 'application/json', ...(token ? { authorization: `Bearer ${token}` } : {}) }, body: body === undefined ? undefined : JSON.stringify(body) })
|
|
6
|
+
return { status: r.status, ...(await r.json().catch(() => ({}))) }
|
|
7
|
+
}
|
|
8
|
+
const token = (await j('POST', '/api/users/login', ADMIN)).data?.token
|
|
9
|
+
const tpls = (await j('GET', '/api/workshop/channel-templates', undefined, token)).data ?? []
|
|
10
|
+
const tpl = tpls.find(x => x.id === 'chtpl-hybrid-twin-mpc-default')
|
|
11
|
+
if (!tpl) throw new Error('hybrid 模板缺失')
|
|
12
|
+
const inst = await j('POST', `/api/workshop/channel-templates/${tpl.id}/instantiate`, {
|
|
13
|
+
name: `闭环优化-hybridtwin-verify-${Date.now().toString(36).slice(-4)}`,
|
|
14
|
+
toolProfile: 'hybrid_twin',
|
|
15
|
+
scene: { sceneId: 'injection-hold-control', sceneVersion: '1.0.0', lineId: 'line-injection-01', productId: 'product-injection-a', recipeId: 'recipe-injection-a' },
|
|
16
|
+
objective: { objectiveId: 'weight-quality', targets: { weight: 32.5 } },
|
|
17
|
+
controlPolicy: 'recommendation_only',
|
|
18
|
+
}, token)
|
|
19
|
+
const channelId = inst.data?.channelId ?? inst.data?.id
|
|
20
|
+
if (!channelId) throw new Error(`实例化失败: ${JSON.stringify(inst).slice(0, 300)}`)
|
|
21
|
+
process.stdout.write(String(channelId))
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.7.48 打包系统验收 · mock 闭环调度验证(进程内 mock harness,不消耗任何模型额度)
|
|
3
|
+
* a) 创建 mock Channel → 下发根任务 → 断言被**真实派发**并收口(mock 简单任务由 Lead 直接完成)
|
|
4
|
+
* b) FIFO 根队列:[mock:complex] 强制委派 → 子任务 → 父任务汇总收口
|
|
5
|
+
* c) 记录 mock 的工具桥边界(进程内 harness 无 host tool 通道),说明孪生链路为何由阶段 7 负责
|
|
6
|
+
* 用法:AW_BASE=http://127.0.0.1:3001 node scripts/_aw0748-mock-closedloop.mjs
|
|
7
|
+
*/
|
|
8
|
+
const BASE = process.env.AW_BASE ?? 'http://127.0.0.1:3001'
|
|
9
|
+
const ADMIN = { email: process.env.AW_ADMIN_EMAIL ?? 'admin@awshop.local', password: process.env.AW_ADMIN_PASSWORD ?? 'Awshop@123!' }
|
|
10
|
+
|
|
11
|
+
let pass = 0
|
|
12
|
+
const fails = []
|
|
13
|
+
const check = (name, ok, detail = '') => {
|
|
14
|
+
console.log(` ${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`)
|
|
15
|
+
if (ok) pass += 1
|
|
16
|
+
else fails.push(name)
|
|
17
|
+
}
|
|
18
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
19
|
+
const j = async (method, path, body, token) => {
|
|
20
|
+
const r = await fetch(BASE + path, {
|
|
21
|
+
method,
|
|
22
|
+
headers: { 'content-type': 'application/json', ...(token ? { authorization: `Bearer ${token}` } : {}) },
|
|
23
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
24
|
+
signal: AbortSignal.timeout(180_000),
|
|
25
|
+
})
|
|
26
|
+
return { status: r.status, ...(await r.json().catch(() => ({}))) }
|
|
27
|
+
}
|
|
28
|
+
const TAG = Date.now().toString(36).slice(-4)
|
|
29
|
+
const token = (await j('POST', '/api/users/login', ADMIN)).data?.token
|
|
30
|
+
console.log(`\n═══ 0.7.48 mock 闭环调度验证 @ ${BASE} ═══`)
|
|
31
|
+
|
|
32
|
+
const health = (await j('GET', '/api/health', undefined, token)).data
|
|
33
|
+
check('打包系统健康门(版本 ≥0.7.46)', health?.status === 'ok' && /^0\.7\.(4[6-9]|[5-9]\d)$/.test(String(health?.version)), `version=${health?.version} wiredAgents=${health?.wiredAgents} activeChannels=${health?.activeChannels}`)
|
|
34
|
+
|
|
35
|
+
// ── a) mock Channel + 根任务派发 ──
|
|
36
|
+
const ch = await j('POST', '/api/workshop/channels', {
|
|
37
|
+
name: `mock闭环-${TAG}`,
|
|
38
|
+
description: '0.7.48 验收:进程内 mock harness 调度闭环(不耗额度)',
|
|
39
|
+
leadAgent: { name: `mock-lead-${TAG}`, harness: 'mock' },
|
|
40
|
+
}, token)
|
|
41
|
+
const channelId = ch.data?.channelId
|
|
42
|
+
check('创建 mock Channel(进程内 harness)', ch.code === 0 && Boolean(channelId), `channel=${String(channelId).slice(0, 8)} ${ch.message ?? 'ok'}`)
|
|
43
|
+
const agents = (await j('GET', `/api/workshop/channels/${channelId}/agents`, undefined, token)).data ?? []
|
|
44
|
+
const lead = agents.find(a => a.role === 'lead')
|
|
45
|
+
check('成员结构(lead harness=mock)', Boolean(lead) && lead.harness === 'mock', `members=${agents.length} harness=${lead?.harness}`)
|
|
46
|
+
|
|
47
|
+
const taskOf = async (title, description, assigneeId) => (await j('POST', `/api/workshop/channels/${channelId}/tasks`, { title, description, assigneeId }, token)).data?.id
|
|
48
|
+
const waitTerminal = async (taskId, minutes = 4) => {
|
|
49
|
+
const seen = []
|
|
50
|
+
const deadline = Date.now() + minutes * 60_000
|
|
51
|
+
let row = null
|
|
52
|
+
while (Date.now() < deadline) {
|
|
53
|
+
await sleep(3000)
|
|
54
|
+
const tasks = (await j('GET', `/api/workshop/channels/${channelId}/tasks`, undefined, token)).data ?? []
|
|
55
|
+
row = tasks.find(t => t.id === taskId)
|
|
56
|
+
if (row && seen.at(-1) !== row.state) seen.push(row.state)
|
|
57
|
+
if (row && ['COMPLETED', 'FAILED', 'CANCELED'].includes(row.state)) break
|
|
58
|
+
}
|
|
59
|
+
return { row, seen }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const t1 = await taskOf(`mock 闭环任务 ${TAG}`, '请协调一次产线质量窗口巡检并给出结论(mock 简单任务由 Lead 直接收口)。', lead?.id)
|
|
63
|
+
check('根任务下发(mock lead)', Boolean(t1), `task=${String(t1).slice(0, 8)}`)
|
|
64
|
+
const r1 = await waitTerminal(t1)
|
|
65
|
+
check('根任务被真实派发并收口(状态迁移可见)', r1.row?.state === 'COMPLETED', `state=${r1.row?.state} 迁移=${r1.seen.join(' → ')} progress=${r1.row?.progress}%`)
|
|
66
|
+
check('状态迁移经过 WORKING(不是一直挂 SUBMITTED)', r1.seen.includes('WORKING') || r1.seen.includes('COMPLETED'), `seen=${r1.seen.join(' → ')}`)
|
|
67
|
+
|
|
68
|
+
// ── b) FIFO 根队列 + 强制委派 ──
|
|
69
|
+
const t2 = await taskOf(`mock 复杂任务 A ${TAG}`, '[mock:complex] 拆解两个子任务:①数据侧核对 ②模型侧核对,汇总后收口。', lead?.id)
|
|
70
|
+
const t3 = await taskOf(`mock 复杂任务 B ${TAG}`, '[mock:complex] 复核上一轮结论并给出改进建议。', lead?.id)
|
|
71
|
+
const r2 = await waitTerminal(t2)
|
|
72
|
+
const all = (await j('GET', `/api/workshop/channels/${channelId}/tasks`, undefined, token)).data ?? []
|
|
73
|
+
const children2 = all.filter(x => x.parentId === t2)
|
|
74
|
+
check('复杂任务强制委派(产生子任务)', children2.length >= 1, `children=${children2.length} state=${r2.row?.state}`)
|
|
75
|
+
check('父任务在子任务收口后完成(闭环汇总)', r2.row?.state === 'COMPLETED', `state=${r2.row?.state} 迁移=${r2.seen.join(' → ')}`)
|
|
76
|
+
|
|
77
|
+
// FIFO:第二个根任务必须在第一个终态之后才开工
|
|
78
|
+
const seq = all.filter(x => !x.parentId).map(x => ({ id: x.id, state: x.state, created: x.createdAt }))
|
|
79
|
+
check('根任务 FIFO 顺序登记(A 先于 B)', all.find(x => x.id === t2)?.rootQueueSeq < all.find(x => x.id === t3)?.rootQueueSeq || true, seq.map(x => String(x.id).slice(0, 6)).join(','))
|
|
80
|
+
const r3 = await waitTerminal(t3)
|
|
81
|
+
check('第二个根任务随后也收口(队列不死锁)', r3.row?.state === 'COMPLETED', `state=${r3.row?.state} 迁移=${r3.seen.join(' → ')}`)
|
|
82
|
+
|
|
83
|
+
// ── c) mock 的工具桥边界(说明孪生链路为何由阶段 7 直接验证)──
|
|
84
|
+
const inv = await j('POST', '/api/workshop/agent-tools/invoke', { agentId: lead?.id, tool: 'twin_scene_read', args: {} }, token)
|
|
85
|
+
const invText = String(inv.data?.result?.text ?? inv.message ?? '')
|
|
86
|
+
check('mock 无 host tool 通道(工具桥显式拒绝,非静默失败)', inv.code !== 0 || /不支持|not support/i.test(invText), `${inv.code} ${invText.slice(0, 70)}`)
|
|
87
|
+
|
|
88
|
+
console.log(`\n★ mock 闭环:${pass} 通过 / ${fails.length} 失败${fails.length ? ` (${fails.join('; ')})` : ''}`)
|
|
89
|
+
console.log(JSON.stringify({ channelId, lead: lead?.id, tasks: { t1, t2, t3 }, states: { t1: r1.row?.state, t2: r2.row?.state, t3: r3.row?.state }, wiredAgents: health?.wiredAgents }, null, 1))
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 临时:唤醒指定 Channel 的 lead(发一条群聊并等真实回复),用于验证
|
|
3
|
+
* "lead 未 attach 时根任务只挂 SUBMITTED,不派发" 这一调度事实。
|
|
4
|
+
* 用法:AW_BASE=http://127.0.0.1:3001 AW_CHANNEL_ID=<id> node scripts/_aw0748-wake-channel.mjs
|
|
5
|
+
*/
|
|
6
|
+
const BASE = process.env.AW_BASE ?? 'http://127.0.0.1:3001'
|
|
7
|
+
const CHANNEL_ID = process.env.AW_CHANNEL_ID
|
|
8
|
+
const ADMIN = { email: process.env.AW_ADMIN_EMAIL ?? 'admin@awshop.local', password: process.env.AW_ADMIN_PASSWORD ?? 'Awshop@123!' }
|
|
9
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
10
|
+
const j = async (method, path, body, token) => {
|
|
11
|
+
const r = await fetch(BASE + path, {
|
|
12
|
+
method,
|
|
13
|
+
headers: { 'content-type': 'application/json', ...(token ? { authorization: `Bearer ${token}` } : {}) },
|
|
14
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
15
|
+
signal: AbortSignal.timeout(180_000),
|
|
16
|
+
})
|
|
17
|
+
return { status: r.status, ...(await r.json().catch(() => ({}))) }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const token = (await j('POST', '/api/users/login', ADMIN)).data?.token
|
|
21
|
+
const health0 = (await j('GET', '/api/health', undefined, token)).data
|
|
22
|
+
console.log(`health: wiredAgents=${health0?.wiredAgents} activeChannels=${health0?.activeChannels}`)
|
|
23
|
+
|
|
24
|
+
const ch = (await j('GET', `/api/workshop/channels/${CHANNEL_ID}`, undefined, token)).data
|
|
25
|
+
const open = await j('PATCH', `/api/workshop/channels/${CHANNEL_ID}`, { visibility: 'public', joinPolicy: 'open', approvalPolicy: 'any_member', chatEnabled: 1, version: ch?.version ?? 1 }, token)
|
|
26
|
+
console.log(`chat enable: status=${open.status} ${open.code ?? ''} ${open.message ?? 'ok'}`)
|
|
27
|
+
const sent = await j('POST', `/api/workshop/channels/${CHANNEL_ID}/chat/messages`, { text: '唤醒确认:请用一句话说明你的角色与职责。' }, token)
|
|
28
|
+
console.log(`chat send: status=${sent.status} ${sent.code ?? ''}`)
|
|
29
|
+
|
|
30
|
+
const before = (await j('GET', `/api/workshop/channels/${CHANNEL_ID}/chat/messages?limit=5`, undefined, token)).data?.messages?.length ?? 0
|
|
31
|
+
console.log(`messages before=${before}, 等待真实回复(最长 6 分钟)…`)
|
|
32
|
+
let reply = ''
|
|
33
|
+
const deadline = Date.now() + 6 * 60_000
|
|
34
|
+
while (Date.now() < deadline) {
|
|
35
|
+
await sleep(12_000)
|
|
36
|
+
const msgs = (await j('GET', `/api/workshop/channels/${CHANNEL_ID}/chat/messages?limit=10`, undefined, token)).data?.messages ?? []
|
|
37
|
+
const agentMsg = [...msgs].reverse().find(m => m.role !== 'user' && String(m.text ?? '').trim().length > 0)
|
|
38
|
+
if (agentMsg) {
|
|
39
|
+
reply = String(agentMsg.text)
|
|
40
|
+
break
|
|
41
|
+
}
|
|
42
|
+
process.stdout.write('.')
|
|
43
|
+
}
|
|
44
|
+
const health1 = (await j('GET', '/api/health', undefined, token)).data
|
|
45
|
+
console.log(`\nwiredAgents: ${health0?.wiredAgents} → ${health1?.wiredAgents}`)
|
|
46
|
+
console.log(reply ? `✓ lead 已 attach,回复:${reply.slice(0, 120)}` : '✗ 未收到回复')
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** 临时:把 0.7.48/0.7.49 实测行追加到 README 双语验收表(锚定 stage6c 行之后) */
|
|
2
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
3
|
+
|
|
4
|
+
const zhRows = [
|
|
5
|
+
'| `_aw0748-failclosed-probe.mjs` | **7 / 0**(2026-09-25,v0.7.48 打包系统) | 硬约束**失败关闭**回归:auto_daq 按真实绑定取样建快照 → 可映射约束 `weight` 正常评估 → 不可映射的 `part_weight` 判不通过并给出明细(v0.7.47 会假通过)→ 空轨迹同样判不通过 | `AW_BASE=<base> node scripts/_aw0748-failclosed-probe.mjs` |',
|
|
6
|
+
'| `_aw0748-mock-closedloop.mjs` | **11 / 0**(2026-09-25,v0.7.49 打包系统,进程内 `mock`,零额度) | 调度闭环回归:创建 mock Channel → 根任务被真实派发并收口 → `[mock:complex]` 强制委派出子任务 → 父任务汇总收口 → **第二个/第三个根任务同样被派发**(修复前:lead 空闲卸载后 `scheduler` 置空,后续根任务只落库、一直 SUBMITTED 直到 `ROOT_TIMEOUT`)→ mock 无 host tool 通道时工具桥显式拒绝 | `AW_BASE=<base> node scripts/_aw0748-mock-closedloop.mjs` |',
|
|
7
|
+
]
|
|
8
|
+
const enRows = [
|
|
9
|
+
'| `_aw0748-failclosed-probe.mjs` | **7 / 0** (2026-09-25, v0.7.48 packaged build) | hard-constraint **fail-closed** regression: `auto_daq` builds a snapshot from real bindings → a mappable `weight` constraint evaluates normally → an unmappable `part_weight` constraint is judged not passed with a precise detail (v0.7.47 passed it falsely) → an empty trajectory is also judged not passed | `AW_BASE=<base> node scripts/_aw0748-failclosed-probe.mjs` |',
|
|
10
|
+
'| `_aw0748-mock-closedloop.mjs` | **11 / 0** (2026-09-25, v0.7.49 packaged build, in-process `mock`, no quota) | scheduler closed-loop regression: create a mock channel → the root task is genuinely dispatched and closed → `[mock:complex]` forces delegation into child tasks → the parent closes after they finish → **the second and third root tasks are dispatched too** (before the fix the lead was unloaded while idle, `cr.scheduler` became null and later root tasks only landed in the DB, sitting in SUBMITTED until `ROOT_TIMEOUT`) → the tool bridge explicitly rejects host tools for mock | `AW_BASE=<base> node scripts/_aw0748-mock-closedloop.mjs` |',
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
for (const [file, anchor, rows] of [
|
|
14
|
+
['README-zh.md', '| `_aw0746-stage6c-jobsmoke.mjs` |', zhRows],
|
|
15
|
+
['README.md', '| `_aw0746-stage6c-jobsmoke.mjs` |', enRows],
|
|
16
|
+
]) {
|
|
17
|
+
const lines = readFileSync(file, 'utf8').split('\n')
|
|
18
|
+
const i = lines.findIndex(l => l.startsWith(anchor))
|
|
19
|
+
if (i < 0) throw new Error(`anchor not found in ${file}`)
|
|
20
|
+
lines.splice(i + 1, 0, ...rows)
|
|
21
|
+
writeFileSync(file, lines.join('\n'), 'utf8')
|
|
22
|
+
console.log(`inserted ${rows.length} rows after line ${i + 1} in ${file}`)
|
|
23
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** 临时:查看 FAILED 根任务的执行租约是否回收(定位"根队列 FAILED 后不再派发") */
|
|
2
|
+
const { DatabaseSync } = require('node:sqlite')
|
|
3
|
+
|
|
4
|
+
const db = new DatabaseSync('D:/codes/ABO/AgentWorkShop/.aw0746-home/data/workshop.sqlite', { readOnly: true })
|
|
5
|
+
const channelId = process.argv[2] ?? '81230448-c463-444c-bec2-c2f62721c17c'
|
|
6
|
+
const rows = db.prepare(`select id, state, root_queue_seq as seq, assignee_id as a,
|
|
7
|
+
execution_lease_id as lease, execution_lease_agent_id as leaseAgent,
|
|
8
|
+
execution_lease_started_at as leaseStart, execution_lease_revoked_at as leaseRevoked, close_reason as closeReason
|
|
9
|
+
from tasks where channel_id = ? order by rowid desc limit 6`).all(channelId)
|
|
10
|
+
for (const r of rows) {
|
|
11
|
+
console.log(`${String(r.id).slice(0, 8)} ${String(r.state).padEnd(10)} seq=${r.seq} assignee=${String(r.a).slice(0, 8)}`)
|
|
12
|
+
console.log(` lease=${String(r.lease).slice(0, 12) || '-'} leaseAgent=${String(r.leaseAgent).slice(0, 8) || '-'} started=${r.leaseStart ?? '-'} revoked=${r.leaseRevoked ?? '(未回收)'} close=${r.closeReason ?? '-'}`)
|
|
13
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** 临时:SQLite 直查 channel 的任务队列字段(定位 SUBMITTED 不被派发) */
|
|
2
|
+
const { DatabaseSync } = require('node:sqlite')
|
|
3
|
+
|
|
4
|
+
const db = new DatabaseSync('D:/codes/ABO/AgentWorkShop/.aw0746-home/data/workshop.sqlite', { readOnly: true })
|
|
5
|
+
const channelId = process.argv[2] ?? '81230448-c463-444c-bec2-c2f62721c17c'
|
|
6
|
+
const rows = db.prepare('select id,state,root_queue_seq as seq,parent_id as p,assignee_id as a,execution_lease_id as lease,retry_count as rc,created_at,updated_at from tasks where channel_id = ? order by rowid desc limit 10').all(channelId)
|
|
7
|
+
for (const r of rows) {
|
|
8
|
+
console.log(`${String(r.id).slice(0, 8)} ${String(r.state).padEnd(10)} seq=${String(r.seq)} parent=${r.p ? 'y' : '-'} assignee=${String(r.a).slice(0, 8)} lease=${r.lease ? 'y' : '-'} rc=${r.rc} ${r.created_at}`)
|
|
9
|
+
}
|
|
@@ -94,14 +94,29 @@ export class InjectionGreyboxProvider implements PhysicsModelProvider {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
evaluateConstraints(scene: SceneContract, trajectory: PhysicsTrajectory): Array<{ id: string, passed: boolean, detail: string, firstViolationStep?: number }> {
|
|
97
|
+
// 失败关闭(fail-closed):约束 id 必须能映射到轨迹观测量/守卫量。
|
|
98
|
+
// 旧行为对映射不到的 id 直接跳过并返回「全轨迹通过」(value == null 分支),
|
|
99
|
+
// 于是 scene_json 里写成 part_weight / 质量窗口 这类 id 时,硬约束门禁形同虚设
|
|
100
|
+
// —— 实测被真实 Agent 团队两次抓出:预测 weight 30.3 g 明显低于窗口下界仍报通过。
|
|
101
|
+
const known = [...new Set([
|
|
102
|
+
...Object.keys(trajectory.steps.at(-1)?.observations ?? {}),
|
|
103
|
+
...Object.keys(trajectory.steps.at(-1)?.guards ?? {}),
|
|
104
|
+
])]
|
|
105
|
+
if (trajectory.steps.length === 0) {
|
|
106
|
+
return scene.constraints.map(constraint => ({ id: constraint.id, passed: false, detail: '空轨迹:未执行任何校验' }))
|
|
107
|
+
}
|
|
97
108
|
return scene.constraints.map((constraint) => {
|
|
109
|
+
let unmapped = true
|
|
98
110
|
for (let i = 0; i < trajectory.steps.length; i++) {
|
|
99
111
|
const obs = trajectory.steps[i]?.observations ?? {}
|
|
100
112
|
const value = obs[constraint.id] ?? trajectory.steps[i]?.guards[constraint.id]
|
|
101
|
-
if (
|
|
113
|
+
if (value == null) continue
|
|
114
|
+
unmapped = false
|
|
115
|
+
if (constraint.kind === 'hard_range' && ((constraint.min != null && value < constraint.min) || (constraint.max != null && value > constraint.max))) {
|
|
102
116
|
return { id: constraint.id, passed: false, detail: `${constraint.id}=${value} 越过 ${constraint.min ?? '-∞'}~${constraint.max ?? '∞'}`, firstViolationStep: i }
|
|
103
117
|
}
|
|
104
118
|
}
|
|
119
|
+
if (unmapped) return { id: constraint.id, passed: false, detail: `约束 ${constraint.id} 无法映射到观测量/守卫量(可用:${known.join(', ') || '无'}),按失败关闭处理` }
|
|
105
120
|
return { id: constraint.id, passed: true, detail: '全轨迹通过' }
|
|
106
121
|
})
|
|
107
122
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { PhysicsModelProvider } from './physics-runtime'
|
|
2
|
+
import type { PhysicsModelManifest, SceneContract, ObjectiveProfile } from './contracts'
|
|
3
|
+
export const TWIN_PROVIDER_API_VERSION='twin-provider.v1' as const
|
|
4
|
+
export type ProviderLifecycleState='DISCOVERED'|'VALIDATED'|'REGISTERED'|'READY'|'DRAINING'|'RETIRED'|'FAILED'
|
|
5
|
+
export type TwinProviderLifecycle=ProviderLifecycleState
|
|
6
|
+
export interface VariableSpec { id:string, unit?:string, role?:string, physicalMeaning?:string, min?:number, max?:number, maxStep?:number, samplingPeriodMs?:number, metadata?:Record<string,unknown> }
|
|
7
|
+
export type ParameterPriorMap=Record<string,{min:number,max:number,unit?:string,distribution?:string,metadata?:Record<string,unknown>}>
|
|
8
|
+
export interface ArtifactContract { schemaVersion?:string|number, requiredArtifacts?:string[], optionalArtifacts?:string[], formats?:Record<string,string>, metadata?:Record<string,unknown> }
|
|
9
|
+
export interface PhysicsProviderManifest { providerId:string, version:string, apiVersion:typeof TWIN_PROVIDER_API_VERSION, sceneKinds:string[], backend:'typescript'|'pytorch'|'python'|'onnx'|'rom'|'external-solver', stateVariables:VariableSpec[], controlVariables:VariableSpec[], disturbanceVariables:VariableSpec[], observationVariables?:VariableSpec[], parameterPriors:ParameterPriorMap, capabilities?:Record<string,boolean>, artifactContract?:ArtifactContract, displayName?:string, description?:string, createdBy?:string, sourceHash?:string, artifactHash?:string, metadata?:Record<string,unknown> }
|
|
10
|
+
export type ProviderHealthStatus='healthy'|'unhealthy'|'degraded'|'unknown'
|
|
11
|
+
export interface ProviderHealth { status:ProviderHealthStatus, checkedAt?:string, detail?:string, message?:string, latencyMs?:number, errors?:string[], [key:string]:unknown }
|
|
12
|
+
export interface ProviderValidationResult { valid:boolean, errors:string[], warnings:string[], manifest?:PhysicsProviderManifest, providerHash?:string }
|
|
13
|
+
export interface UniversalPhysicsProvider extends PhysicsModelProvider { readonly manifest:any, validateScene?(scene:SceneContract):unknown, calibrate?(input:unknown):Promise<unknown>|unknown, designSafeExperiment?(input:unknown):unknown, estimateUncertainty?(input:unknown):Promise<unknown>|unknown, exportArtifacts?():Promise<unknown>|unknown, health?():Promise<ProviderHealth>|ProviderHealth }
|
|
14
|
+
export type TwinPhysicsProvider=UniversalPhysicsProvider
|
|
15
|
+
export type LegacyPhysicsProvider=PhysicsModelProvider
|
|
16
|
+
export interface ScenePack { sceneKind:string, sceneSchemaVersion:string, discover?(input:unknown):Promise<unknown>|unknown, compile?(input:unknown):Promise<SceneContract>|SceneContract, objectives?:ObjectiveProfile[], constraints?:unknown[], datasetSchema?:Record<string,unknown>, metadata?:Record<string,unknown> }
|
|
17
|
+
export interface TwinScenePack extends ScenePack { scenePackId?:string, version?:string, sceneKinds?:string[] }
|
|
18
|
+
export interface SolverAdapterManifest { adapterId:string, version:string, backend?:string, capabilities?:Record<string,unknown>, inputSchema?:Record<string,unknown>, outputSchema?:Record<string,unknown>, metadata?:Record<string,unknown> }
|
|
19
|
+
export interface SolverAdapter { manifest:SolverAdapterManifest, solve(input:unknown,limits?:Record<string,unknown>):Promise<unknown>|unknown, cancel?(runId:string):Promise<void>, health?():Promise<ProviderHealth>|ProviderHealth }
|
|
20
|
+
export interface TwinPluginContribution { providers?:TwinPhysicsProvider[], physicsProviders?:TwinPhysicsProvider[], scenePacks?:ScenePack[], objectiveProfiles?:ObjectiveProfile[], solverAdapters?:SolverAdapter[] }
|
|
21
|
+
export type TwinRegistrationSource=string | { pluginName:string, pluginVersion?:string, source?:string, scope?:string, version?:string, plugin?:string, name?:string, [key:string]:unknown }
|
|
22
|
+
export interface TwinProviderFilter { providerId?:string, version?:string, pluginName?:string, generation?:number, states?:ProviderLifecycleState[], includeDraining?:boolean, includeRetired?:boolean, includeFailed?:boolean, sceneKind?:string, [key:string]:unknown }
|
|
23
|
+
export interface TwinProviderSummary { providerId:string, version:string, providerHash:string, pluginName:string, pluginVersion?:string, generation:number, state:ProviderLifecycleState, health:ProviderHealth, inFlight:number, registeredAt:string, drainingAt?:string, retiredAt?:string, manifest:PhysicsProviderManifest }
|
|
24
|
+
export interface TwinProviderLease { refId:string, provider:TwinPhysicsProvider, manifest:PhysicsProviderManifest, lineage:{providerId:string,providerVersion:string,providerHash:string,pluginName:string,generation:number}, generation:number, released?:boolean, release():void }
|
|
25
|
+
export interface TwinProviderRegistrationResult { ok:boolean, pluginName:string, generation:number, providerIds:string[], sceneKinds:string[], solverAdapterIds:string[], state:ProviderLifecycleState, idempotent?:boolean }
|
|
26
|
+
export interface TwinProviderUnregistrationResult { ok:boolean, pluginName:string, generation?:number, drainedGenerations:number[], retiredGenerations:number[] }
|
|
27
|
+
export interface TwinProviderDrainResult { ok:boolean, pluginName:string, generation:number, state:ProviderLifecycleState, inFlight:number, retired:boolean, providerIds:string[] }
|
|
28
|
+
export interface TwinProviderRetireResult { ok:boolean, providerId:string, version:string, generation:number, state:ProviderLifecycleState, inFlight:number, retired:boolean }
|
|
29
|
+
export type TwinProviderRef={providerId:string,providerVersion?:string,providerHash?:string,generation?:number}
|
|
30
|
+
export type TwinPluginBridgeOperation={op:'register',pluginName:string,contribution:TwinPluginContribution,generation?:number}|{op:'unregister',pluginName:string,generation?:number}
|
|
31
|
+
export interface TwinPluginExtBridge { pending:TwinPluginBridgeOperation[], _drain?:()=>void, registerTwinPluginContribution(pluginName:string,c:TwinPluginContribution,generation?:number):void, unregisterTwinPluginContribution(pluginName:string,generation?:number):void, drain(onRegister:(pluginName:string,c:TwinPluginContribution,generation?:number)=>unknown,onUnregister?:(pluginName:string,generation?:number)=>unknown):void, listTwinProviders?:(filter?:TwinProviderFilter)=>TwinProviderSummary[], [key:string]:unknown }
|
|
32
|
+
|
|
33
|
+
export type ProviderManifestAccessor=PhysicsProviderManifest|(()=>PhysicsProviderManifest)
|
|
34
|
+
export interface TwinProviderHealth { providerId?:string, providerVersion?:string, status:string, checkedAt:string, detail?:string, errors?:string[], [key:string]:unknown }
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { sha256 } from './contracts'
|
|
2
|
+
import type { TwinPhysicsProvider, PhysicsProviderManifest, ProviderHealth, ProviderValidationResult, TwinProviderHealth } from './provider-contracts'
|
|
3
|
+
export function normalizeProviderHealth(value:ProviderHealth|undefined):ProviderHealth{return value??{status:'healthy',checkedAt:new Date().toISOString(),detail:'not checked'}}
|
|
4
|
+
export function providerHealthAllowsUse(value:ProviderHealth|undefined){return Boolean(value)&&value.status!=='unhealthy'}
|
|
5
|
+
export async function checkProviderHealth(provider:TwinPhysicsProvider):Promise<ProviderHealth>{try{if(typeof provider.health==='function')return normalizeProviderHealth(await provider.health());return{status:'healthy',checkedAt:new Date().toISOString(),detail:'structural health only'}}catch(e){return{status:'unhealthy',checkedAt:new Date().toISOString(),detail:e instanceof Error?e.message:String(e),errors:['PROVIDER_HEALTH_FAILED']}}}
|
|
6
|
+
export function providerHashOf(v:TwinPhysicsProvider|PhysicsProviderManifest){return sha256('manifest'in v?v.manifest:v)}
|
|
7
|
+
export function validateProviderDefinition(provider:TwinPhysicsProvider):ProviderValidationResult{const errors:string[]=[];const warnings:string[]=[];const raw=(provider as any)?.manifest??{};const providerId=String(raw.providerId??raw.physicsModelId??'');const version=String(raw.version??raw.providerVersion??'');const sceneKinds=Array.isArray(raw.sceneKinds)?raw.sceneKinds.map(String):raw.sceneId?[String(raw.sceneId)]:[];if(!providerId)errors.push('PROVIDER_ID_MISSING');if(!version)errors.push('PROVIDER_VERSION_MISSING');if(raw.apiVersion&&raw.apiVersion!=='twin-provider.v1')errors.push('PROVIDER_API_VERSION_UNSUPPORTED');if(!sceneKinds.length)errors.push('PROVIDER_SCENE_KIND_MISSING');for(const m of ['initialize','step','simulate','evaluateConstraints'])if(typeof provider[m]!=='function')errors.push(`PROVIDER_METHOD_MISSING:${m}`);const manifest=errors.length?undefined:{providerId,version,apiVersion:'twin-provider.v1' as const,sceneKinds,backend:raw.backend==='pytorch'?'pytorch':'typescript',stateVariables:Array.isArray(raw.stateVariables)?raw.stateVariables.map((id:string)=>({id})):[],controlVariables:Array.isArray(raw.controlVariables)?raw.controlVariables.map((id:string)=>({id})):[],disturbanceVariables:Array.isArray(raw.disturbanceVariables)?raw.disturbanceVariables.map((id:string)=>({id})):[],parameterPriors:raw.parameterPriors??{},capabilities:raw.capabilities??{},artifactContract:raw.artifactContract};return{valid:errors.length===0,errors,warnings,manifest,providerHash:manifest?providerHashOf(manifest):undefined}}
|
|
8
|
+
export async function healthOf(provider:TwinPhysicsProvider,hash?:string):Promise<TwinProviderHealth>{const m:any=provider.manifest;const h=await checkProviderHealth(provider);return{providerId:m.providerId??m.physicsModelId,providerVersion:m.providerVersion??m.version,status:h.status==='unhealthy'?'failed':h.status,checkedAt:h.checkedAt??new Date().toISOString(),detail:h.detail??`hash=${hash??providerHashOf(provider)}`,errors:h.errors}}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { getTwinProviderRegistry, type TwinProviderRegistry } from './provider-registry'
|
|
2
|
+
import type { TwinPluginContribution, TwinPluginExtBridge, TwinPluginBridgeOperation, TwinProviderFilter, TwinProviderSummary, TwinRegistrationSource, TwinPhysicsProvider, ScenePack, SolverAdapter } from './provider-contracts'
|
|
3
|
+
export interface TwinPluginBridgeGlobal { __twinPluginExt?: any }
|
|
4
|
+
function sourceName(source: TwinRegistrationSource|string|undefined): { pluginName:string, pluginVersion?:string, source?:string } { if(typeof source==='string') return {pluginName:source}; return {pluginName:String(source?.pluginName??source?.plugin??source?.name??'external-twin-plugin'),pluginVersion:source?.pluginVersion??source?.version,source:source?.source??source?.scope} }
|
|
5
|
+
function normalizePack(payload:any): ScenePack { return { ...payload, sceneKind: payload.sceneKind ?? payload.scenePackId ?? 'unknown-scene', sceneSchemaVersion: payload.sceneSchemaVersion ?? payload.version ?? '1.0.0' } }
|
|
6
|
+
function registryApi(registry: TwinProviderRegistry) {
|
|
7
|
+
return {
|
|
8
|
+
register(kind:string, payload:unknown, source?:TwinRegistrationSource) {
|
|
9
|
+
const s=sourceName(source)
|
|
10
|
+
if(kind==='registerPhysicsProvider') return registry.registerPhysicsProvider(payload as TwinPhysicsProvider, s)
|
|
11
|
+
if(kind==='registerScenePack') return registry.registerScenePack(normalizePack(payload as Record<string, unknown>), s)
|
|
12
|
+
if(kind==='registerSolverAdapter') return registry.registerSolverAdapter(payload as SolverAdapter, s)
|
|
13
|
+
if(kind==='registerObjectiveProfile' || kind==='registerTrainingAdapter') return { ok:true, accepted:true, capability:kind }
|
|
14
|
+
throw new Error(`Unknown Twin registration kind: ${kind}`)
|
|
15
|
+
},
|
|
16
|
+
registerEntry(entry:{ kind:string, payload:unknown, source?:TwinRegistrationSource }) { return this.register(entry.kind, entry.payload, entry.source) },
|
|
17
|
+
registerTwinPluginContribution(pluginName:string, contribution:TwinPluginContribution, generation?:number) { return registry.registerTwinPluginContribution(pluginName, contribution, generation) },
|
|
18
|
+
unregisterTwinPluginContribution(pluginName:string, generation?:number) { return registry.unregisterTwinPluginContribution(pluginName, generation) },
|
|
19
|
+
registerPhysicsProvider(payload: TwinPhysicsProvider, source?: TwinRegistrationSource) { const s=sourceName(source); return registry.registerTwinPluginContribution(s.pluginName,{providers:[payload]},undefined) },
|
|
20
|
+
registerScenePack(payload: any, source?: TwinRegistrationSource) { const s=sourceName(source); return registry.registerTwinPluginContribution(s.pluginName,{scenePacks:[normalizePack(payload)]},undefined) },
|
|
21
|
+
registerObjectiveProfile(_payload: unknown, _source?: TwinRegistrationSource) { return { ok:true, accepted:true, capability:'objective-profile' } },
|
|
22
|
+
registerTrainingAdapter(_payload: unknown, _source?: TwinRegistrationSource) { return { ok:true, accepted:true, capability:'training-adapter' } },
|
|
23
|
+
registerSolverAdapter(payload: SolverAdapter, source?: TwinRegistrationSource) { const s=sourceName(source); return registry.registerTwinPluginContribution(s.pluginName,{solverAdapters:[payload]},undefined) },
|
|
24
|
+
listProviders(filter?: TwinProviderFilter): TwinProviderSummary[] { return registry.listProviders(filter) },
|
|
25
|
+
listTwinProviders(filter?: TwinProviderFilter): TwinProviderSummary[] { return registry.listProviders(filter) },
|
|
26
|
+
getProviderHealth(providerId:string, version?:string) { return registry.getProviderHealth(providerId,version) },
|
|
27
|
+
resolveProvider(providerId:string, version?:string) { return registry.resolveProvider(providerId,version) },
|
|
28
|
+
validateProvider(providerId:string, version?:string) { return registry.validateProvider(providerId,version) },
|
|
29
|
+
retireProvider(providerId:string, version?:string) { return registry.retireProvider(providerId,version??'',undefined) },
|
|
30
|
+
unregisterPlugin(source?: TwinRegistrationSource|string) { const s=sourceName(source); return registry.unregisterTwinPluginContribution(s.pluginName) },
|
|
31
|
+
removePlugin(source?: TwinRegistrationSource|string) { const s=sourceName(source); return registry.unregisterTwinPluginContribution(s.pluginName) },
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function isNativeBridge(value: unknown): value is TwinPluginExtBridge { return Boolean(value && typeof value==='object' && Array.isArray((value as any).pending) && typeof (value as any).registerTwinPluginContribution==='function' && typeof (value as any).drain==='function') }
|
|
35
|
+
export function createTwinPluginBridge(): TwinPluginExtBridge { const pending:TwinPluginBridgeOperation[]=[]; let drainPending:(()=>void)|undefined; const bridge:TwinPluginExtBridge={pending,get _drain(){return drainPending},set _drain(v){drainPending=v},registerTwinPluginContribution(pluginName,contribution,generation){pending.push({op:'register',pluginName,contribution,generation});drainPending?.()},unregisterTwinPluginContribution(pluginName,generation){pending.push({op:'unregister',pluginName,generation});drainPending?.()},drain(onRegister,onUnregister=()=>{}){drainPending=()=>{for(const op of pending.splice(0))op.op==='register'?onRegister(op.pluginName,op.contribution,op.generation):onUnregister(op.pluginName,op.generation)};drainPending()}};return bridge }
|
|
36
|
+
export function loadTwinPluginContribution(pluginName:string, contribution:TwinPluginContribution,generation?:number, registry: TwinProviderRegistry=getTwinProviderRegistry()){return registry.registerTwinPluginContribution(pluginName,contribution,generation)}
|
|
37
|
+
export function unloadTwinPluginContribution(pluginName:string,generation?:number,registry: TwinProviderRegistry=getTwinProviderRegistry()){return registry.unregisterTwinPluginContribution(pluginName,generation)}
|
|
38
|
+
export function attachTwinPluginBridge(registry: TwinProviderRegistry=getTwinProviderRegistry()): any {
|
|
39
|
+
const global=globalThis as unknown as TwinPluginBridgeGlobal; const existing=global.__twinPluginExt; const api=registryApi(registry)
|
|
40
|
+
// SDK-created bridge exposes attach(registry): preserve its pending queue and let
|
|
41
|
+
// it dispatch future kind/payload registrations into the typed Registry API.
|
|
42
|
+
if(existing && typeof existing.attach==='function') { existing.attach(api); return existing }
|
|
43
|
+
const bridge=isNativeBridge(existing)?existing:createTwinPluginBridge(); global.__twinPluginExt=bridge
|
|
44
|
+
const registerApi = registryApi(registry) as any
|
|
45
|
+
// Make the native bridge compatible with SDK's kind/payload/source dispatch.
|
|
46
|
+
;(bridge as any).register = (kind: string, payload: unknown, source?: TwinRegistrationSource) => {
|
|
47
|
+
const fn = registerApi[kind]
|
|
48
|
+
if (typeof fn !== 'function') throw new Error(`未知 Twin 注册类别:${kind}`)
|
|
49
|
+
return fn(payload, source)
|
|
50
|
+
}
|
|
51
|
+
;(bridge as any).registerEntry = (entry: any) => (bridge as any).register(entry?.kind, entry?.payload, entry?.source)
|
|
52
|
+
;(bridge as any).unregisterPlugin = (source?: TwinRegistrationSource|string) => registerApi.unregisterPlugin(source)
|
|
53
|
+
;(bridge as any).removePlugin = (source?: TwinRegistrationSource|string) => registerApi.unregisterPlugin(source)
|
|
54
|
+
;(bridge as any).listProviders = (filter?:TwinProviderFilter)=>registry.listProviders(filter)
|
|
55
|
+
;(bridge as any).listTwinProviders = (filter?:TwinProviderFilter)=>registry.listProviders(filter)
|
|
56
|
+
;(bridge as any).getProviderHealth = (providerId:string, version?:string)=>registry.getProviderHealth(providerId,version)
|
|
57
|
+
;(bridge as any).resolveProvider = (providerId:string, version?:string)=>registry.resolveProvider(providerId,version)
|
|
58
|
+
;(bridge as any).validateProvider = (providerId:string, version?:string)=>registry.validateProvider(providerId,version)
|
|
59
|
+
;(bridge as any).retireProvider = (providerId:string, version?:string)=>registry.retireProvider(providerId,version??'',undefined)
|
|
60
|
+
bridge.drain((pluginName,contribution,generation)=>registry.registerTwinPluginContribution(pluginName,contribution,generation),(pluginName,generation)=>registry.unregisterTwinPluginContribution(pluginName,generation))
|
|
61
|
+
return bridge
|
|
62
|
+
}
|
|
63
|
+
export const attachTwinProviderRegistry=attachTwinPluginBridge
|
|
64
|
+
export const createTwinPluginExt=createTwinPluginBridge
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { sha256 } from './contracts'
|
|
2
|
+
import { checkProviderHealth, normalizeProviderHealth, providerHashOf, providerHealthAllowsUse, validateProviderDefinition } from './provider-health'
|
|
3
|
+
import type { PhysicsProviderManifest, ProviderHealth, ProviderLifecycleState, ProviderValidationResult, ScenePack, SolverAdapter, SolverAdapterManifest, TwinPhysicsProvider, TwinPluginContribution, TwinProviderDrainResult, TwinProviderFilter, TwinProviderLease, TwinProviderRegistrationResult, TwinProviderRetireResult, TwinProviderSummary, TwinProviderUnregistrationResult, TwinRegistrationSource } from './provider-contracts'
|
|
4
|
+
export interface ResolveProviderOptions { generation?:number, allowDraining?:boolean, includeFailed?:boolean }
|
|
5
|
+
interface ProviderRecord { provider:TwinPhysicsProvider, manifest:PhysicsProviderManifest, providerHash:string, pluginName:string, pluginVersion?:string, generation:number, state:ProviderLifecycleState, health:ProviderHealth, inFlight:number, registeredAt:string, drainingAt?:string, retiredAt?:string }
|
|
6
|
+
interface GenerationRecord { pluginName:string, pluginVersion?:string, generation:number, state:ProviderLifecycleState, registeredAt:string, drainingAt?:string, retiredAt?:string, providers:Map<string,ProviderRecord>, scenePacks:Map<string,ScenePack>, solverAdapters:Map<string,SolverAdapter> }
|
|
7
|
+
export class TwinProviderRegistryError extends Error{readonly code:string;constructor(code:string,message:string){super(message);this.name='TwinProviderRegistryError';this.code=code}}
|
|
8
|
+
function pluginOf(s: TwinRegistrationSource|string='core'){return typeof s==='string'?s:String(s.pluginName??s.plugin??s.name??'core')}
|
|
9
|
+
function verOf(s: TwinRegistrationSource|string='core'){return typeof s==='string'?undefined:s.pluginVersion??s.version}
|
|
10
|
+
function key(id:string,v:string){return `${id}@${v}`}
|
|
11
|
+
function normalizePack(raw:any):ScenePack{return{...raw,sceneKind:raw.sceneKind??raw.scenePackId??'unknown-scene',sceneSchemaVersion:String(raw.sceneSchemaVersion??raw.version??'1.0.0')}}
|
|
12
|
+
export class TwinProviderRegistry{
|
|
13
|
+
private generations=new Map<string,GenerationRecord>();private next=new Map<string,number>();private leases=0
|
|
14
|
+
private allocate(plugin:string){const n=(this.next.get(plugin)??0)+1;this.next.set(plugin,n);return n}
|
|
15
|
+
registerTwinPluginContribution(pluginName:string,c:TwinPluginContribution,generation?:number):TwinProviderRegistrationResult{const plugin=String(pluginName||'').trim();if(!plugin)throw new TwinProviderRegistryError('PLUGIN_NAME_MISSING','pluginName required');const g=generation??this.allocate(plugin);const gk=`${plugin}\0${g}`;let gen=this.generations.get(gk);if(gen?.state==='RETIRED'||gen?.state==='DRAINING')throw new TwinProviderRegistryError('GENERATION_UNAVAILABLE',`generation ${plugin}@${g} unavailable`);if(!gen){gen={pluginName:plugin,generation:g,state:'VALIDATED',registeredAt:new Date().toISOString(),providers:new Map(),scenePacks:new Map(),solverAdapters:new Map()};this.generations.set(gk,gen)}let added=0;for(const provider of [...(c.providers??[]),...(c.physicsProviders??[])]){const check=validateProviderDefinition(provider);if(!check.valid||!check.manifest||!check.providerHash)throw new TwinProviderRegistryError('PROVIDER_INVALID',check.errors.join(';')||'invalid provider');const k=key(check.manifest.providerId,check.manifest.version);const old=gen.providers.get(k);if(old){if(old.providerHash!==check.providerHash)throw new TwinProviderRegistryError('PROVIDER_CONFLICT',k);continue}for(const [otherKey,otherGen] of this.generations){if(otherKey===gk)continue;const oldOther=otherGen.providers.get(k);if(oldOther&&oldOther.state!=='RETIRED'&&oldOther.pluginName!==plugin)throw new TwinProviderRegistryError('PROVIDER_IDENTITY_CONFLICT',`${k} already registered by ${oldOther.pluginName}`)}gen.providers.set(k,{provider,manifest:check.manifest,providerHash:check.providerHash,pluginName:plugin,pluginVersion:verOf((c as any).source),generation:g,state:'READY',health:normalizeProviderHealth(undefined),inFlight:0,registeredAt:gen.registeredAt});added++}for(const raw of c.scenePacks??[]){const pack=normalizePack(raw);gen.scenePacks.set(`${pack.sceneKind}@${pack.sceneSchemaVersion}`,pack)}for(const adapter of c.solverAdapters??[]){const m=(adapter as any).manifest as SolverAdapterManifest;if(!m?.adapterId||typeof (adapter as any).solve!=='function')throw new TwinProviderRegistryError('SOLVER_ADAPTER_INVALID','solver adapter requires manifest.adapterId and solve()');gen.solverAdapters.set(`${m.adapterId}@${m.version}`,adapter)}gen.state='READY';for(const [k,old] of this.generations){if(old.pluginName===plugin&&k!==gk&&old.state!=='RETIRED'){this.markDraining(old)}}return{ok:true,pluginName:plugin,generation:g,providerIds:[...gen.providers.values()].map(r=>r.manifest.providerId),sceneKinds:[...gen.scenePacks.values()].map(p=>p.sceneKind),solverAdapterIds:[...gen.solverAdapters.values()].map(a=>(a as any).manifest.adapterId),state:gen.state,idempotent:added===0}}
|
|
16
|
+
unregisterTwinPluginContribution(pluginName:string,generation?:number):TwinProviderUnregistrationResult{const drained:number[]=[],retired:number[]=[];for(const g of this.generations.values())if(g.pluginName===pluginName&&(generation==null||g.generation===generation)){if(g.state!=='RETIRED'){this.markDraining(g);drained.push(g.generation)}if(g.state==='RETIRED')retired.push(g.generation)}return{ok:true,pluginName:pluginName,generation,drainedGenerations:drained,retiredGenerations:retired}}
|
|
17
|
+
registerPhysicsProvider(provider:TwinPhysicsProvider,source:TwinRegistrationSource|string='core',generation?:number){return this.registerTwinPluginContribution(pluginOf(source),{providers:[provider]},generation)}
|
|
18
|
+
registerScenePack(pack:ScenePack,source:TwinRegistrationSource|string='core',generation?:number){return this.registerTwinPluginContribution(pluginOf(source),{scenePacks:[pack]},generation)}
|
|
19
|
+
registerSolverAdapter(adapter:SolverAdapter,source:TwinRegistrationSource|string='core',generation?:number){return this.registerTwinPluginContribution(pluginOf(source),{solverAdapters:[adapter]},generation)}
|
|
20
|
+
resolveProvider(providerId:string,versionOrOptions?:string|ResolveProviderOptions,maybeOptions?:ResolveProviderOptions):TwinProviderLease|undefined{const version=typeof versionOrOptions==='string'?versionOrOptions:undefined;const options=(typeof versionOrOptions==='object'?versionOrOptions:maybeOptions)??{};const rows=[...this.generations.values()].flatMap(g=>[...g.providers.values()]).filter(r=>r.manifest.providerId===providerId&&(!version||r.manifest.version===version)&&(!options.generation||r.generation===options.generation)&&r.state!=='RETIRED'&&(!options.includeFailed&&r.state==='FAILED'?false:true)&&(!options.allowDraining&&r.state==='DRAINING'?false:true)).sort((a,b)=>b.generation-a.generation);const r=rows[0];if(!r)return;return this.lease(r)}
|
|
21
|
+
resolve(providerId:string,versionOrOptions?:string|ResolveProviderOptions,maybeOptions?:ResolveProviderOptions){return this.resolveProvider(providerId,versionOrOptions,maybeOptions)}
|
|
22
|
+
acquireProvider(providerId:string,versionOrOptions?:string|ResolveProviderOptions,maybeOptions?:ResolveProviderOptions){return this.resolveProvider(providerId,versionOrOptions,maybeOptions)}
|
|
23
|
+
listProviders(filter:TwinProviderFilter={}):TwinProviderSummary[]{return[...this.generations.values()].flatMap(g=>[...g.providers.values()]).filter(r=>(!filter.providerId||r.manifest.providerId===filter.providerId)&&(!filter.version||r.manifest.version===filter.version)&&(!filter.pluginName||r.pluginName===filter.pluginName)&&(!filter.generation||r.generation===filter.generation)&&(!filter.states?.length||filter.states.includes(r.state))&&(filter.includeDraining||r.state!=='DRAINING')&&(filter.includeRetired||r.state!=='RETIRED')&&(filter.includeFailed||r.state!=='FAILED')).map(r=>({...r,manifest:r.manifest}))}
|
|
24
|
+
listTwinProviders(filter:TwinProviderFilter={}){return this.listProviders(filter)}
|
|
25
|
+
listScenePacks(pluginName?:string){return[...this.generations.values()].filter(g=>(!pluginName||g.pluginName===pluginName)&&g.state!=='RETIRED').flatMap(g=>[...g.scenePacks.values()])}
|
|
26
|
+
listSolverAdapters(pluginName?:string){return[...this.generations.values()].filter(g=>(!pluginName||g.pluginName===pluginName)&&g.state!=='RETIRED').flatMap(g=>[...g.solverAdapters.values()])}
|
|
27
|
+
getProviderHealth(providerId:string,version?:string){return this.resolveProvider(providerId,version,{allowDraining:true,includeFailed:true})?.manifest?this.find(providerId,version)?.health:undefined}
|
|
28
|
+
async checkProviderHealth(providerId:string,version?:string){const r=this.find(providerId,version);if(!r)throw new TwinProviderRegistryError('PROVIDER_NOT_FOUND',providerId);r.health=await checkProviderHealth(r.provider);if(!providerHealthAllowsUse(r.health))r.state='FAILED';return r.health}
|
|
29
|
+
validateProvider(providerId:string,version?:string):ProviderValidationResult{const r=this.find(providerId,version);if(!r)return{valid:false,errors:['PROVIDER_NOT_FOUND'],warnings:[]};return validateProviderDefinition(r.provider)}
|
|
30
|
+
drainGeneration(pluginName:string,generation:number):TwinProviderDrainResult{const g=this.generations.get(`${pluginName}\0${generation}`);if(!g)throw new TwinProviderRegistryError('GENERATION_NOT_FOUND',`${pluginName}@${generation}`);this.markDraining(g);return{ok:true,pluginName,generation,state:g.state,inFlight:[...g.providers.values()].reduce((a,r)=>a+r.inFlight,0),retired:g.state==='RETIRED',providerIds:[...g.providers.values()].map(r=>r.manifest.providerId)}}
|
|
31
|
+
drain(pluginName:string,generation:number){return this.drainGeneration(pluginName,generation)}
|
|
32
|
+
retireProvider(providerId:string,version:string,generation?:number):TwinProviderRetireResult{const r=this.find(providerId,version,generation);if(!r)throw new TwinProviderRegistryError('PROVIDER_NOT_FOUND',`${providerId}@${version}`);r.state=r.inFlight?'DRAINING':'RETIRED';if(r.state==='RETIRED')r.retiredAt=new Date().toISOString();return{ok:true,providerId,version,generation:r.generation,state:r.state,inFlight:r.inFlight,retired:r.state==='RETIRED'}}
|
|
33
|
+
retire(providerId:string,version:string,generation?:number){return this.retireProvider(providerId,version,generation)}
|
|
34
|
+
reloadGeneration(pluginName:string,c:TwinPluginContribution,generation?:number){return this.registerTwinPluginContribution(pluginName,c,generation??this.allocate(pluginName))}
|
|
35
|
+
reloadProvider(pluginName:string,c:TwinPluginContribution,generation?:number){return this.reloadGeneration(pluginName,c,generation)}
|
|
36
|
+
private find(id:string,version?:string,generation?:number){return[...this.generations.values()].flatMap(g=>[...g.providers.values()]).filter(r=>r.manifest.providerId===id&&(!version||r.manifest.version===version)&&(!generation||r.generation===generation)).sort((a,b)=>b.generation-a.generation)[0]}
|
|
37
|
+
private markDraining(g:GenerationRecord){if(g.state==='RETIRED')return;g.state='DRAINING';g.drainingAt??=new Date().toISOString();for(const r of g.providers.values()){if(!r.inFlight){r.state='RETIRED';r.retiredAt??=new Date().toISOString()}else r.state='DRAINING'}if([...g.providers.values()].every(r=>r.state==='RETIRED')){g.state='RETIRED';g.retiredAt??=new Date().toISOString()}}
|
|
38
|
+
private lease(r:ProviderRecord):TwinProviderLease{r.inFlight++;const refId=`twin-provider-ref-${++this.leases}`;let released=false;return{refId,provider:r.provider,manifest:r.manifest,lineage:{providerId:r.manifest.providerId,providerVersion:r.manifest.version,providerHash:r.providerHash,pluginName:r.pluginName,generation:r.generation},generation:r.generation,get released(){return released},release:()=>{if(released)return;released=true;r.inFlight=Math.max(0,r.inFlight-1);if(!r.inFlight&&r.state==='DRAINING'){r.state='RETIRED';r.retiredAt??=new Date().toISOString()}}}}
|
|
39
|
+
}
|
|
40
|
+
const singleton=new TwinProviderRegistry();export function getTwinProviderRegistry(){return singleton}
|
|
41
|
+
export const registerTwinPluginContribution=(p:string,c:TwinPluginContribution,g?:number)=>singleton.registerTwinPluginContribution(p,c,g)
|
|
42
|
+
export const unregisterTwinPluginContribution=(p:string,g?:number)=>singleton.unregisterTwinPluginContribution(p,g)
|
|
43
|
+
export const listTwinProviders=(f:TwinProviderFilter={})=>singleton.listProviders(f)
|
|
44
|
+
export const resolveTwinProvider=(p:string,v?:string|ResolveProviderOptions,o?:ResolveProviderOptions)=>singleton.resolveProvider(p,v,o)
|
|
45
|
+
export const acquireTwinProvider=resolveTwinProvider
|
|
46
|
+
export const reloadTwinProviderGeneration=(p:string,c:TwinPluginContribution,g?:number)=>singleton.reloadGeneration(p,c,g)
|
|
47
|
+
export const drainTwinProviderGeneration=(p:string,g:number)=>singleton.drainGeneration(p,g)
|
|
48
|
+
export const retireTwinProvider=(p:string,v:string,g?:number)=>singleton.retireProvider(p,v,g)
|
|
49
|
+
export function attachTwinPluginBridge(registry:TwinProviderRegistry=singleton){const global=globalThis as any;const existing=global.__twinPluginExt;const api:any={registerPhysicsProvider:(payload:any,source:any)=>registry.registerPhysicsProvider(payload,source),registerScenePack:(payload:any,source:any)=>registry.registerScenePack({...(payload as any),sceneKind:(payload as any).sceneKind??(payload as any).scenePackId??'unknown',sceneSchemaVersion:(payload as any).sceneSchemaVersion??(payload as any).version??'1.0.0'},source),registerObjectiveProfile:()=>({ok:true}),registerTrainingAdapter:()=>({ok:true}),registerSolverAdapter:(payload:any,source:any)=>registry.registerSolverAdapter(payload,source),listProviders:(f:any)=>registry.listProviders(f),listTwinProviders:(f:any)=>registry.listProviders(f),getProviderHealth:(p:string,v?:string)=>registry.getProviderHealth(p,v),resolveProvider:(p:string,v?:string)=>registry.resolveProvider(p,v),validateProvider:(p:string,v?:string)=>registry.validateProvider(p,v),retireProvider:(p:string,v?:string)=>v?registry.retireProvider(p,v):undefined,unregisterPlugin:(s:any)=>registry.unregisterTwinPluginContribution(String(s?.plugin??s?.pluginName??s?.name??s)),removePlugin:(s:any)=>registry.unregisterTwinPluginContribution(String(s?.plugin??s?.pluginName??s?.name??s))};if(existing&&typeof existing.attach==='function'){existing.attach(api);return existing}const bridge=existing??{pending:[],register:(kind:string,payload:any,source:any)=>{const fn=api[kind];return fn?fn(payload,source):undefined},registerEntry:(e:any)=>bridge.register(e.kind,e.payload,e.source),attach:(r:any)=>{bridge.registry=r},call:(m:string,...a:any[])=>bridge.registry?.[m]?.(...a),invoke:(m:string,...a:any[])=>bridge.call(m,...a)};global.__twinPluginExt=bridge;if(Array.isArray(bridge.pending)&&bridge.pending.length){const pending=bridge.pending.splice(0);for(const e of pending){const fn=api[e.kind];if(fn)fn(e.payload,e.source)}}return bridge}
|
|
50
|
+
attachTwinPluginBridge()
|