@aiwuk/team 0.0.0-stage → 5.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/LICENSE +21 -0
  3. package/README.md +149 -2
  4. package/README.zh-CN.md +150 -0
  5. package/access.js +93 -0
  6. package/activity-summary.js +36 -0
  7. package/allocation.js +34 -0
  8. package/board-dispatch.js +141 -0
  9. package/brand.js +41 -0
  10. package/client/00-core-zh.js +284 -0
  11. package/client/01-i18n-en.js +265 -0
  12. package/client/02-styles.js +336 -0
  13. package/client/02b-stage-styles.js +65 -0
  14. package/client/03-shared.js +82 -0
  15. package/client/03b-collapsible-list.js +34 -0
  16. package/client/03c-config-panel.js +605 -0
  17. package/client/04-tool-cards.js +110 -0
  18. package/client/04a-dispatch-confirm.js +162 -0
  19. package/client/04b-more-cards.js +78 -0
  20. package/client/04c-model-picker.js +68 -0
  21. package/client/04d-confirm-dock.js +40 -0
  22. package/client/05-policy.js +217 -0
  23. package/client/06-usage.js +195 -0
  24. package/client/06b-model-source.js +67 -0
  25. package/client/07-board-parts.js +312 -0
  26. package/client/07a-role-theme.js +25 -0
  27. package/client/08-artifacts-report.js +375 -0
  28. package/client/08a-task-control.js +126 -0
  29. package/client/08b-stage-sprites.js +97 -0
  30. package/client/08c-stage-colors.js +55 -0
  31. package/client/08c-stage-scene.js +238 -0
  32. package/client/08d-stage-adapter.js +65 -0
  33. package/client/08e-stage-activity.js +83 -0
  34. package/client/09-team-board.js +181 -0
  35. package/client/09b-role-view.js +95 -0
  36. package/client/09c-model-tabs.js +20 -0
  37. package/client/10-overview.js +364 -0
  38. package/client/10b-onboarding.js +121 -0
  39. package/client/11-register.js +249 -0
  40. package/client.js +5053 -0
  41. package/config-projection.js +40 -0
  42. package/config.js +411 -0
  43. package/cordis.patch.yml +110 -0
  44. package/defaults.js +222 -0
  45. package/dispatch-confirm.js +293 -0
  46. package/dispatch-routes-store.js +31 -0
  47. package/failover.js +85 -0
  48. package/file-limits.js +119 -0
  49. package/free-worker.mjs +125 -0
  50. package/http-guard.js +14 -0
  51. package/i18n/roles.en.json +23 -0
  52. package/i18n/roles.zh.json +23 -0
  53. package/i18n/server.en.json +659 -0
  54. package/i18n/server.zh.json +659 -0
  55. package/i18n.js +34 -0
  56. package/icon.svg +7 -0
  57. package/index.js +111 -0
  58. package/integrations-config.js +301 -0
  59. package/integrations-messages.js +62 -0
  60. package/integrations.js +17 -0
  61. package/lead-route.js +30 -0
  62. package/locale/en.json +6 -0
  63. package/locale/zh.json +6 -0
  64. package/long-context.js +30 -0
  65. package/managed-config-route.js +49 -0
  66. package/managed-ref.js +38 -0
  67. package/managed-resolve.js +47 -0
  68. package/member-task-control.js +242 -0
  69. package/memory-backends.js +89 -0
  70. package/memory-embed.js +70 -0
  71. package/memory-hub.js +150 -0
  72. package/memory-openviking.js +101 -0
  73. package/memory-redact.js +34 -0
  74. package/memory-store.js +219 -0
  75. package/memory-sync-queue.js +28 -0
  76. package/model-policy.js +85 -0
  77. package/model-prefs.js +104 -0
  78. package/observability-api.js +76 -0
  79. package/observability-projection.js +141 -0
  80. package/onboarding.js +47 -0
  81. package/opencode.js +74 -0
  82. package/package.json +153 -4
  83. package/plan-gate.js +92 -0
  84. package/preset-guard.js +47 -0
  85. package/request-evidence.js +41 -0
  86. package/roles.js +69 -0
  87. package/roster-defaults.js +70 -0
  88. package/route-health.js +39 -0
  89. package/runtime-observability.js +78 -0
  90. package/runtime-view.js +21 -0
  91. package/scripts/memory-cli.mjs +138 -0
  92. package/search-engines.js +119 -0
  93. package/search.js +164 -0
  94. package/session-policy.js +59 -0
  95. package/session-rotation.js +206 -0
  96. package/settings-schema.js +40 -0
  97. package/shared/managed-config.js +84 -0
  98. package/single-model.js +44 -0
  99. package/skills/project-role-theme/SKILL.md +46 -0
  100. package/task-bridge.js +32 -0
  101. package/task-control-api.js +36 -0
  102. package/task-control-audit.js +60 -0
  103. package/team.js +101 -0
  104. package/teammate-context.js +120 -0
  105. package/teammate-wait.js +18 -0
  106. package/template.js +46 -0
  107. package/theme-plugin.js +172 -0
  108. package/theme.js +115 -0
  109. package/token-discipline.js +116 -0
  110. package/token-metrics.js +37 -0
  111. package/tool-names.js +10 -0
  112. package/tool-schema.js +23 -0
  113. package/transcribe.js +104 -0
  114. package/ui.js +113 -0
  115. package/usage.js +92 -0
  116. package/workflow.js +246 -0
package/i18n.js ADDED
@@ -0,0 +1,34 @@
1
+ // Server text uses the host's explicit locale preference, with an environment override.
2
+ import en from './i18n/server.en.json' with { type: 'json' };
3
+ import zh from './i18n/server.zh.json' with { type: 'json' };
4
+ import rolesEn from './i18n/roles.en.json' with { type: 'json' };
5
+ import rolesZh from './i18n/roles.zh.json' with { type: 'json' };
6
+ const dictionaries = { en, zh };
7
+ const roles = { en: rolesEn, zh: rolesZh };
8
+ let hostSettings;
9
+ export const normalizeLocale = value => /^zh(?:[-_]|$)/i.test(String(value ?? '')) ? 'zh' : 'en';
10
+ /** Bind only the public, optional DSH settings service; read preferences live. */
11
+ export function configureI18n(ctx) {
12
+ ctx.inject?.(['settings'], scoped => {
13
+ hostSettings = scoped.settings;
14
+ scoped.effect?.(() => () => { if (hostSettings === scoped.settings) hostSettings = undefined; });
15
+ });
16
+ }
17
+ export function locale({ env = process.env, settings = hostSettings } = {}) {
18
+ if (env.AIWUK_LOCALE) return normalizeLocale(env.AIWUK_LOCALE);
19
+ try {
20
+ const rows = settings?.describe?.({ redactSecrets: true }) ?? [];
21
+ return normalizeLocale(rows.find(row => row.ns === 'locale')?.value?.preference);
22
+ } catch { return 'en'; }
23
+ }
24
+ const interpolate = (text, vars) => text.replace(/\{(\w+)\}/g, (match, key) => Object.hasOwn(vars, key) ? String(vars[key]) : match);
25
+ /** Translate user-visible text. Unknown keys remain visible for diagnostics. */
26
+ export function t(key, vars = {}, language = locale()) {
27
+ return interpolate(dictionaries[normalizeLocale(language)][key] ?? dictionaries.en[key] ?? key, vars);
28
+ }
29
+ /** Tool descriptions, errors and model instructions are always English. */
30
+ export const m = (key, vars = {}) => t(key, vars, 'en');
31
+ /** Role/persona data is distinct from user-interface text. */
32
+ export function roleText(key, vars = {}, language = locale()) {
33
+ return interpolate(roles[normalizeLocale(language)][key] ?? roles.en[key] ?? key, vars);
34
+ }
package/icon.svg ADDED
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" fill="none">
2
+ <rect x="4" y="4" width="56" height="56" rx="14" fill="#1e1b4b" stroke="#6366f1" stroke-width="2"/>
3
+ <path d="M16 48 L32 16 L48 48 Z" fill="rgba(99, 102, 241, 0.3)" stroke="#f59e0b" stroke-width="2"/>
4
+ <circle cx="32" cy="22" r="4" fill="#38bdf8"/>
5
+ <circle cx="24" cy="40" r="3.5" fill="#10b981"/>
6
+ <circle cx="40" cy="40" r="3.5" fill="#ef4444"/>
7
+ </svg>
package/index.js ADDED
@@ -0,0 +1,111 @@
1
+ import { apply as applyRoles } from './roles.js';
2
+ import { ownsCapabilityTeam } from './preset-guard.js';
3
+ import { apply as applyIntegrations } from './integrations.js';
4
+ import { apply as applyTheme } from './theme-plugin.js';
5
+ import { createTeam } from './team.js';
6
+ import { createFailover } from './failover.js';
7
+ import { createWorkflow } from './workflow.js';
8
+ import { applyAccess } from './access.js';
9
+ import { applyTeammateWait } from './teammate-wait.js';
10
+ import { createPlanGate } from './plan-gate.js';
11
+ import { createRuntimeObservability } from './runtime-observability.js';
12
+ import { createTaskControl } from './member-task-control.js';
13
+ import { createTaskAudit } from './task-control-audit.js';
14
+ import { createDispatchConfirm, setRouteStore } from './dispatch-confirm.js';
15
+ import { createRouteStore } from './dispatch-routes-store.js';
16
+ import { applyModelPrefsTool, createModelPrefs } from './model-prefs.js';
17
+ import { createBoardDispatch } from './board-dispatch.js';
18
+ import { baseName } from './session-rotation.js';
19
+ import { legacyRoleId } from './roster-defaults.js';
20
+ import { applySkillCatalog, scopeTools } from './teammate-context.js';
21
+ import { createTokenDiscipline } from './token-discipline.js';
22
+ import { createRouteHealth, mergeHealth } from './route-health.js';
23
+ import { createRotation } from './session-rotation.js';
24
+ import { createMemoryHub } from './memory-hub.js';
25
+ import { knownSecretValues, refreshCredentials, releaseCredentials, useCredentials } from './integrations-config.js';
26
+ export const inject = ['agents', 'agentTeams', 'systemPrompt', 'tools', 'workingDirectory', 'skills', 'llm', 'userQuestions'];
27
+ import { configureI18n } from './i18n.js';
28
+ export function apply(host, config = {}) {
29
+ configureI18n(host);
30
+ // Lead-only and teammate-only plugin tools are installed per agent, so teammates do not carry the lead's tool schemas.
31
+ const { ctx } = scopeTools(host);
32
+ const team = createTeam(ctx, config); // Validates preset bindings up front.
33
+ // Memory endpoints/models are read-only user file/environment configuration.
34
+ // Keys come from DSH's own credential store first (Settings → credentials), the environment second.
35
+ // The same binding also feeds the managed manifest reader, so locked fields win over local settings.
36
+ ctx.inject?.(['credentials'], scoped => {
37
+ const service = scoped.credentials;
38
+ if (!service) return;
39
+ useCredentials(service);
40
+ scoped.effect?.(() => () => releaseCredentials(service));
41
+ void refreshCredentials().catch(() => {});
42
+ });
43
+ // Settings may also arrive as same-named credentials (e.g. MEMORY_BACKEND pushed by a fleet manager).
44
+ void refreshCredentials().catch(() => {});
45
+ void team.refresh(); // First catalog match in the background; requests await it if still pending.
46
+ const observability = createRuntimeObservability();
47
+ const routeHealth = createRouteHealth(team.root);
48
+ const failover = createFailover({
49
+ telemetry: event => { observability.telemetry(event); routeHealth.record(event); },
50
+ // A provider missing from DSH throws before the stream starts, which no hook can recover.
51
+ registered: provider => ctx.llm.listProviders().some(p => p.id === provider),
52
+ log: message => ctx.logger?.info?.(`capability-team: ${message}`),
53
+ });
54
+ const taskControl = createTaskControl(ctx, team, { audit: createTaskAudit(team.root), onChanged: () => observability.notifyTaskControl(),
55
+ predecessorOf: (leadId, name) => rotation.predecessorOf(leadId, name) });
56
+ const planGate = createPlanGate(ctx, team);
57
+ // Re-read the credential store before redacting, so a key added in Settings since the last refresh is redacted too.
58
+ const memory = createMemoryHub(ctx, team, { secrets: async () => { await refreshCredentials().catch(() => {}); return knownSecretValues(); } });
59
+ const rotation = createRotation(ctx, team, { onHandoff: handoff => memory.ingestHandoff(handoff) });
60
+ const board = createBoardDispatch(ctx, team);
61
+ const prefs = createModelPrefs(team.root);
62
+ setRouteStore(createRouteStore(team.root)); // per-task confirmed routes survive a DSH restart
63
+ const workflow = createWorkflow(ctx, team, { onDelegate: (lead, roles) => planGate.reset(lead, roles), onAssignments: (lead, assignments) => taskControl.dispatch(lead, assignments), dispatchConfirm: createDispatchConfirm(ctx, team, { prefs, health: () => mergeHealth(routeHealth.recent(), failover.health()) }), rotation, board });
64
+ taskControl.setWorkflow(workflow);
65
+ taskControl.register();
66
+ workflow.register();
67
+ planGate.register();
68
+ rotation.register();
69
+ memory.register();
70
+ applyBoard(ctx, team, board, rotation);
71
+ applyModelPrefsTool(ctx, team, prefs);
72
+ createTokenDiscipline(host, team).register();
73
+ applySkillCatalog(host, agent => {
74
+ const project = team.pinned(agent);
75
+ const m = host.agentTeams.tryMembership(agent);
76
+ if (!project || !m?.name) return undefined;
77
+ const { effective } = team.loadSync(project, agent);
78
+ const id = baseName(m.name);
79
+ return effective.capabilities[effective.roles[legacyRoleId(id, effective.roles) ?? id]?.capability]?.skills;
80
+ });
81
+ applyAccess(ctx, team);
82
+ applyTeammateWait(ctx);
83
+ applyRoles(ctx, team, failover, observability, taskControl);
84
+ applyIntegrations(ctx, team);
85
+ applyTheme(ctx, team, failover, workflow, planGate, taskControl);
86
+ }
87
+ /**
88
+ * Board integration hooks. A teammate becomes an active member once it claims its first message; then its ready
89
+ * tasks are handed to it (and a rotation successor takes over its predecessor's open tasks). Its runtime context
90
+ * lists its board tasks, and edits outside the scopes of the tasks it holds are refused when configured.
91
+ */
92
+ function applyBoard(ctx, team, board, rotation) {
93
+ ctx.on('agent/inbox/claimed', ({ agent }) => {
94
+ const m = ctx.agentTeams.tryMembership(agent);
95
+ if (!ownsCapabilityTeam(ctx, agent) || m?.role !== 'teammate' || !m.root) return;
96
+ const from = rotation.predecessorOf(m.root.id, m.name);
97
+ void (async () => { if (from) await board.transfer(m.root, from, m.name); await board.handOut(m.root, baseName(m.name), m.name); })().catch(() => {});
98
+ });
99
+ ctx.on('agent/created', ({ agent }) => {
100
+ if (!ownsCapabilityTeam(ctx, agent) || ctx.agentTeams.tryMembership(agent)?.role !== 'teammate') return;
101
+ agent.ctx.systemPrompt.context({ name: 'capability-team:board', order: 96, text: () => board.context(agent) });
102
+ });
103
+ ctx.effect(() => ctx.tools.guard(exec => {
104
+ if (!ownsCapabilityTeam(ctx, exec.agent)) return undefined;
105
+ const project = team.pinned(exec.agent);
106
+ if (!project) return undefined;
107
+ let enforce;
108
+ try { enforce = team.loadSync(project, exec.agent).effective.workflow.enforceWriteScopes !== false; } catch { return undefined; }
109
+ return board.guard(exec, { enforce });
110
+ }));
111
+ }
@@ -0,0 +1,301 @@
1
+ // External integrations (search engines, embeddings, speech, memory) are per-user data, never code: base URLs,
2
+ // paths, headers, models and engine order all come from configuration, and nothing assumes a particular vendor.
3
+ // Keys are never stored: the config names the environment variable (or DSH credential) that holds each key.
4
+ // Layers (contract F.3): a LOCKED managed ref (published by the Mothership connector) wins over everything local;
5
+ // otherwise bundle settings form › credentials › environment › user integrations.json › unlocked managed value ›
6
+ // defaults. Every resolved field reports its source: managed-locked | settings | credential | env | file |
7
+ // managed-default | default. Managed values are ordinary credentials; only the lock manifest is extra.
8
+ import { readFileSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { defaultRoot } from './theme.js';
11
+ import { message } from './integrations-messages.js';
12
+ import { pickLayer, sourceLabel } from './managed-resolve.js';
13
+ import { readManagedManifest } from './shared/managed-config.js';
14
+ /** Search engines in their default (auto) order, and the page-fetch engines. */
15
+ export const SEARCH_ENGINES = Object.freeze(['tavily', 'brave', 'searxng', 'custom']);
16
+ export const FETCH_ENGINES = Object.freeze(['jina', 'tavily', 'custom']);
17
+ export const MEMORY_BACKENDS = Object.freeze(['local', 'openviking']);
18
+ export const SHARED_RECALL_SCOPES = Object.freeze(['current', 'none']);
19
+ export const INTEGRATION_DEFAULTS = Object.freeze({
20
+ // engines: null = auto (every configured engine, in SEARCH_ENGINES order). baseUrl/apiKeyEnv/paths/providers
21
+ // belong to the `custom` engine (any gateway speaking the generic search/fetch protocol). Custom gateways may
22
+ // fall back across upstream providers, so a full answer can take up to two minutes.
23
+ search: Object.freeze({ engines: null, fetchEngines: Object.freeze(['jina']), tavilyKeyEnv: 'TAVILY_API_KEY', braveKeyEnv: 'BRAVE_API_KEY', searxngUrl: null, searxngKeyEnv: 'SEARXNG_API_KEY', jinaKeyEnv: 'JINA_API_KEY',
24
+ baseUrl: null, apiKeyEnv: 'OMNI_SEARCH_API_KEY', searchPath: '/v1/search', fetchPath: '/v1/web/fetch', providersPath: null, headers: {}, providers: [], fetchProviders: [], timeoutMs: 120000, fetchTimeoutMs: 120000,
25
+ // Direct engines (tavily, brave, searxng, jina) answer fast; a short timeout keeps failover snappy.
26
+ engineTimeoutMs: 30000 }),
27
+ // Any OpenAI-compatible /v1/embeddings endpoint (OpenAI, Ollama, vLLM, Jina, a gateway...).
28
+ embeddings: Object.freeze({ baseUrl: null, apiKeyEnv: 'EMBEDDING_API_KEY', path: '/v1/embeddings', headers: {}, model: null, provider: null, dimensions: null, queryTask: null, passageTask: null, timeoutMs: 15000, batch: 32 }),
29
+ // Speech to text: multipart upload of a user-approved local file. maxBytes mirrors a typical gateway cap;
30
+ // the gateway's own limit wins when it is lower (a 413 is reported, never retried).
31
+ audio: Object.freeze({ baseUrl: null, apiKeyEnv: 'OMNI_AUDIO_API_KEY', path: '/v1/audio/transcriptions', statusPath: null, headers: {}, model: null, timeoutMs: 125000, maxBytes: 8388608 }),
32
+ // Memory hub backend (one of MEMORY_BACKENDS): 'local' (team store only) or 'openviking' (also shared
33
+ // long-term memory on that server). The OpenViking key is optional: a local dev-mode server needs none.
34
+ // sharedRecall (one of SHARED_RECALL_SCOPES) is a deployment choice: 'current' keeps recall local because
35
+ // shared backends carry no trusted project metadata; 'none' searches the whole shared account.
36
+ memory: Object.freeze({ backend: 'local', url: null, apiKeyEnv: 'OPENVIKING_API_KEY', account: null, user: null, timeoutMs: 8000, recallLimit: 4, sharedRecall: 'current' }),
37
+ });
38
+ /**
39
+ * Names that override single fields. A list means primary first, then legacy fallbacks: a later name is
40
+ * consulted only when every earlier one is unset. Each name is read from the DSH credential store first (a
41
+ * fleet manager can push settings there) and from the environment second.
42
+ */
43
+ export const ENV_OVERRIDES = Object.freeze({
44
+ search: { baseUrl: 'OMNI_SEARCH_BASE_URL', searxngUrl: 'SEARXNG_URL', engines: 'OMNI_SEARCH_ENGINES', fetchEngines: 'OMNI_FETCH_ENGINES', providersPath: 'OMNI_SEARCH_PROVIDERS_PATH' },
45
+ embeddings: { baseUrl: ['EMBEDDING_BASE_URL', 'CPA_EMBED_BASE_URL'], model: ['EMBEDDING_MODEL', 'CPA_EMBED_MODEL'], dimensions: ['EMBEDDING_DIMENSIONS', 'CPA_EMBED_DIMENSIONS'] },
46
+ audio: { baseUrl: 'OMNI_AUDIO_BASE_URL' },
47
+ memory: { url: 'OPENVIKING_URL', backend: 'MEMORY_BACKEND', sharedRecall: 'MEMORY_SHARED_RECALL' },
48
+ });
49
+ /** Every override name (primary and legacy), e.g. for refreshing them from the credential store. */
50
+ export const overrideNames = () => [...new Set(Object.values(ENV_OVERRIDES).flatMap(fields => Object.values(fields).flat()))];
51
+ // List fields arrive as one comma-separated string from the environment or a credential.
52
+ const LIST_FIELDS = ['engines', 'fetchEngines'];
53
+ const overrideValue = (key, raw) => (LIST_FIELDS.includes(key) ? String(raw).split(',').map(v => v.trim()).filter(Boolean) : raw);
54
+ /** Legacy key names consulted when a section still uses its default key name and that one is unset. */
55
+ export const LEGACY_KEY_ENV = Object.freeze({ embeddings: Object.freeze({ EMBEDDING_API_KEY: 'CPA_EMBED_API_KEY' }) });
56
+ /** Per-engine key fields of integrations.search (the `custom` engine uses apiKeyEnv). */
57
+ export const SEARCH_KEY_FIELDS = Object.freeze({ tavily: 'tavilyKeyEnv', brave: 'braveKeyEnv', searxng: 'searxngKeyEnv', jina: 'jinaKeyEnv', custom: 'apiKeyEnv' });
58
+ const SECTIONS = Object.keys(INTEGRATION_DEFAULTS);
59
+ const KEY_FIELDS = ['apiKeyEnv', 'tavilyKeyEnv', 'braveKeyEnv', 'searxngKeyEnv', 'jinaKeyEnv'];
60
+ const HEADER_NAME = /^[A-Za-z0-9-]{1,64}$/;
61
+ const FORBIDDEN_HEADERS = ['authorization', 'cookie', 'host', 'content-length', 'content-type', 'proxy-authorization'];
62
+ const fail = (label, rule) => { throw new Error(`${label} ${rule}`); };
63
+ const integerIn = (value, min, max, label, rule) => { const n = Number(value); return Number.isSafeInteger(n) && n >= min && n <= max ? n : fail(label, rule); };
64
+ /** An https URL, or plain http only for a server on this machine; never with credentials, query or hash. */
65
+ export function localOrHttpsUrl(value, label) {
66
+ let url; try { url = new URL(String(value)); } catch { fail(label, message('notUrl')); }
67
+ const loopback = ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname);
68
+ if (!(url.protocol === 'https:' || (url.protocol === 'http:' && loopback)) || url.username || url.password || url.search || url.hash) fail(label, message('localOrHttps'));
69
+ return url.href.replace(/\/+$/, '');
70
+ }
71
+ const engineList = (value, allowed, label) => {
72
+ if (!Array.isArray(value) || !value.length || value.some(v => !allowed.includes(v))) fail(label, message('engineList', { allowed: allowed.join(' / ') }));
73
+ return [...new Set(value)];
74
+ };
75
+ function validateField(section, key, value) {
76
+ const label = `${section}.${key}`;
77
+ if (value === null) return null;
78
+ switch (key) {
79
+ case 'engines': return engineList(value, SEARCH_ENGINES, label);
80
+ case 'fetchEngines': return engineList(value, FETCH_ENGINES, label);
81
+ case 'searxngUrl': return localOrHttpsUrl(value, label);
82
+ case 'baseUrl': {
83
+ let url; try { url = new URL(String(value)); } catch { fail(label, message('notUrl')); }
84
+ if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) fail(label, message('httpsOnly'));
85
+ return url.href.replace(/\/+$/, '');
86
+ }
87
+ case 'tavilyKeyEnv': case 'braveKeyEnv': case 'searxngKeyEnv': case 'jinaKeyEnv':
88
+ case 'apiKeyEnv': return /^[A-Z_][A-Z0-9_]{0,63}$/.test(value) ? value : fail(label, message('envName'));
89
+ case 'searchPath': case 'fetchPath': case 'providersPath': case 'statusPath': case 'path':
90
+ return typeof value === 'string' && /^\/[A-Za-z0-9._~\/-]{0,200}$/.test(value) && !value.includes('..') ? value : fail(label, message('path'));
91
+ case 'headers': {
92
+ if (typeof value !== 'object' || Array.isArray(value)) fail(label, message('headersObject'));
93
+ const out = {};
94
+ for (const [name, v] of Object.entries(value)) {
95
+ if (!HEADER_NAME.test(name) || FORBIDDEN_HEADERS.includes(name.toLowerCase())) fail(label, message('headerForbidden', { name }));
96
+ if (typeof v !== 'string' || v.length > 512 || /[\r\n]/.test(v)) fail(label, message('headerValue', { name }));
97
+ out[name] = v;
98
+ }
99
+ return out;
100
+ }
101
+ case 'providers': case 'fetchProviders':
102
+ if (!Array.isArray(value) || value.length > 50 || value.some(v => typeof v !== 'string' || !/^[\w.:-]{1,64}$/.test(v))) fail(label, message('providerList'));
103
+ return [...new Set(value)];
104
+ case 'model': case 'provider': case 'queryTask': case 'passageTask':
105
+ return typeof value === 'string' && value.trim() && value.length <= 200 && !/\s/.test(value.trim()) ? value.trim() : fail(label, message('noWhitespace'));
106
+ case 'dimensions': return integerIn(value, 1, 8192, label, message('intRange', { min: 1, max: 8192 }));
107
+ case 'timeoutMs': case 'fetchTimeoutMs': case 'engineTimeoutMs': return integerIn(value, 1000, 300000, label, message('msRange', { min: 1000, max: 300000 }));
108
+ case 'batch': return integerIn(value, 1, 256, label, message('intRange', { min: 1, max: 256 }));
109
+ case 'maxBytes': return integerIn(value, 1024, 104857600, label, message('byteRange', { min: 1024, max: 104857600 }));
110
+ case 'backend': return MEMORY_BACKENDS.includes(value) ? value : fail(label, message('oneOf', { values: MEMORY_BACKENDS.join(message('or')) }));
111
+ // Plain http is accepted only for a server on this machine; anything remote must use https.
112
+ case 'url': return localOrHttpsUrl(value, label);
113
+ case 'account': case 'user':
114
+ return typeof value === 'string' && /^[\w.@-]{1,64}$/.test(value) ? value : fail(label, message('accountName'));
115
+ case 'recallLimit': return integerIn(value, 0, 20, label, message('intRange', { min: 0, max: 20 }));
116
+ case 'sharedRecall': return SHARED_RECALL_SCOPES.includes(value) ? value : fail(label, message('oneOf', { values: SHARED_RECALL_SCOPES.join(message('or')) }));
117
+ default: return fail(label, message('unsupportedField'));
118
+ }
119
+ }
120
+ // Settings layer: the owning bundle's volatile DSH Config (settings-schema.js), read on every resolve.
121
+ let settingsReader = () => ({});
122
+ export function setSettingsReader(fn) { settingsReader = typeof fn === 'function' ? fn : () => ({}); }
123
+ function readSettings() { try { return settingsReader() ?? {}; } catch { return {}; } }
124
+ /** Validate a partial config ({search?, embeddings?}); unknown sections or fields are rejected. */
125
+ export function validateIntegrations(value) {
126
+ if (value === undefined || value === null) return {};
127
+ if (typeof value !== 'object' || Array.isArray(value)) throw new Error(message('integrationsObject'));
128
+ const out = {};
129
+ for (const [section, fields] of Object.entries(value)) {
130
+ if (!SECTIONS.includes(section)) throw new Error(message('integrationsSection', { section }));
131
+ if (fields === null) continue;
132
+ if (typeof fields !== 'object' || Array.isArray(fields)) throw new Error(message('integrationsSectionObject', { section }));
133
+ out[section] = {};
134
+ for (const [key, v] of Object.entries(fields)) {
135
+ if (!Object.hasOwn(INTEGRATION_DEFAULTS[section], key)) throw new Error(`integrations.${section}.${key} ${message('unsupportedField')}`);
136
+ out[section][key] = validateField(section, key, v);
137
+ }
138
+ }
139
+ return out;
140
+ }
141
+ export const integrationsPath = (root = defaultRoot()) => join(root, 'integrations.json');
142
+ function readFileLayer(root) {
143
+ try { const raw = JSON.parse(readFileSync(integrationsPath(root), 'utf8')); return { revision: raw.revision ?? 0, value: validateIntegrations(raw.integrations) }; }
144
+ catch (error) { if (error.code === 'ENOENT') return { revision: 0, value: {} }; throw new Error(message('fileCorrupt', { reason: error.message })); }
145
+ }
146
+ /**
147
+ * Effective config with the source of every field, plus whether each section's key is present.
148
+ * Key values never leave this function except through `keyOf`.
149
+ */
150
+ export function resolveIntegrations({ root = defaultRoot(), env = process.env, managed, credentials, settings: injectedSettings } = {}) {
151
+ const file = readFileLayer(root);
152
+ const effective = {}, sources = {}, keys = {}, locked = {};
153
+ // The credential store only backs the real process environment (tests pass their own env, or their own store).
154
+ const store = credentials ?? (env === process.env ? credentialCache : undefined);
155
+ const manifest = managed === undefined ? (store ? managedManifest : null) : managed;
156
+ const settings = injectedSettings ?? (store ? readSettings() : {});
157
+ const entryOf = name => manifest?.entries?.get(name);
158
+ for (const section of SECTIONS) {
159
+ effective[section] = {}; sources[section] = {}; locked[section] = {};
160
+ for (const key of Object.keys(INTEGRATION_DEFAULTS[section])) {
161
+ const names = [ENV_OVERRIDES[section][key] ?? []].flat();
162
+ const layer = pickLayer({
163
+ names, entryOf,
164
+ settings: () => settings[section]?.[key],
165
+ credential: name => store?.get(name),
166
+ env: name => env[name],
167
+ file: file.value[section]?.[key],
168
+ // A file value was validated when it was read; every other layer is validated here, and an invalid one is skipped.
169
+ validate: (raw, kind) => (kind === 'file' ? raw : validateField(section, key, kind === 'settings' ? raw : overrideValue(key, raw))),
170
+ fallback: INTEGRATION_DEFAULTS[section][key],
171
+ });
172
+ effective[section][key] = layer.value; sources[section][key] = sourceLabel(layer); locked[section][key] = layer.locked;
173
+ }
174
+ const names = sectionKeyNames(section, effective[section], manifest);
175
+ keys[section] = keyState(names, env, store, manifest, names[0]);
176
+ }
177
+ keys.search.engines = Object.fromEntries(Object.entries(SEARCH_KEY_FIELDS).map(([engine, field]) => {
178
+ const names = sectionKeyNames('search', effective.search, manifest, field);
179
+ return [engine, keyState(names, env, store, manifest, names[0])];
180
+ }));
181
+ const resolved = { revision: file.revision, effective, sources, locked, keys, fileLayer: file.value };
182
+ // The environment, store and manifest stay attached but non-enumerable, so a serialized view never carries them.
183
+ for (const [name, value] of Object.entries({ env, store, managed: manifest })) Object.defineProperty(resolved, name, { value });
184
+ return resolved;
185
+ }
186
+ /** Presence and source of a key (never its value): `{env: <name>, present, source, locked}`. */
187
+ function keyState(names, env, store, manifest, label) {
188
+ const hit = findKey(names, env, store, manifest);
189
+ return { env: label, present: !!hit, source: hit?.source ?? 'default', locked: names.some(name => manifest?.entries?.get(name)?.locked === true) };
190
+ }
191
+ /** A locked canonical credential owns only its section/engine field; local aliases cannot bypass that lock. */
192
+ function sectionKeyNames(section, config, manifest, field = 'apiKeyEnv') {
193
+ const canonical = INTEGRATION_DEFAULTS[section][field];
194
+ if (manifest?.entries?.get(canonical)?.locked === true) return [canonical];
195
+ const name = config[field], legacy = LEGACY_KEY_ENV[section]?.[name];
196
+ return legacy && name === canonical ? [name, legacy] : [name];
197
+ }
198
+ // Each name is tried in order. A locked managed name reads only the store (the managed value); an unlocked managed
199
+ // name lets a local environment value win over it; any other name reads the store before the environment.
200
+ function findKey(names, env, store, manifest) {
201
+ for (const name of names) {
202
+ const entry = manifest?.entries?.get(name), stored = store?.get(name), local = env[name];
203
+ if (entry?.locked) { if (stored) return { value: stored, source: 'managed-locked' }; continue; }
204
+ if (entry) { if (local) return { value: local, source: 'env' }; if (stored) return { value: stored, source: 'managed-default' }; continue; }
205
+ if (stored) return { value: stored, source: 'credential' };
206
+ if (local) return { value: local, source: 'env' };
207
+ }
208
+ return undefined;
209
+ }
210
+ const storeOf = (resolved, env) => (env === resolved.env ? resolved.store : env === process.env ? credentialCache : undefined);
211
+ const lookup = (names, resolved, env) => findKey(names, env, storeOf(resolved, env), resolved.managed)?.value;
212
+ /** The API key for a section, read from the credential/environment variable its config names. */
213
+ export const keyOf = (section, resolved, env = resolved.env ?? process.env) => lookup(sectionKeyNames(section, resolved.effective[section], resolved.managed), resolved, env);
214
+ /** The key of one search/fetch engine (tavily, brave, searxng, jina, custom); undefined when not set. */
215
+ export const engineKeyOf = (engine, resolved, env = resolved.env ?? process.env) => {
216
+ if (engine === 'custom') return keyOf('search', resolved, env);
217
+ const field = SEARCH_KEY_FIELDS[engine];
218
+ return field ? lookup(sectionKeyNames('search', resolved.effective.search, resolved.managed, field), resolved, env) : undefined;
219
+ };
220
+ /** Include effective canonical refs and local aliases for refresh and redaction, even when a lock ignores an alias. */
221
+ export function keyNames(resolved) {
222
+ const names = SECTIONS.flatMap(section => [...sectionKeyNames(section, resolved.effective[section], resolved.managed), ...sectionKeyNames(section, resolved.effective[section])]);
223
+ for (const field of KEY_FIELDS) names.push(...sectionKeyNames('search', resolved.effective.search, resolved.managed, field), resolved.effective.search[field]);
224
+ return [...new Set(names)];
225
+ }
226
+ // DSH's credential store (ctx.credentials, the keys saved in Settings) is consulted first, so every key name
227
+ // is a DSH credential reference and the process environment is the fallback.
228
+ // Values are cached in memory only, refreshed before each request, and only ever placed in a request header.
229
+ let credentialResolver;
230
+ const credentialCache = new Map();
231
+ export function setCredentialResolver(fn) { credentialResolver = fn; credentialCache.clear(); }
232
+ // Managed manifest (lock policy only, never values): fed by an async reader the host plugin supplies (see
233
+ // shared/managed-config.js readManagedManifest) and re-read with the credentials.
234
+ let managedManifest = null, managedReader = null;
235
+ export function setManagedReader(fn) { managedReader = typeof fn === 'function' ? fn : null; if (!managedReader) managedManifest = null; }
236
+ /** Set the manifest directly (tests, or a host that already holds it). */
237
+ export function setManagedManifest(manifest) { managedManifest = manifest ?? null; }
238
+ export const managedManifestOf = () => managedManifest;
239
+ /**
240
+ * Bind a DSH credential service (`ctx.credentials`): key names resolve through `resolve`, and the managed manifest is
241
+ * read through `readRecord` (with the file fallback). Pass null to unbind. Replaces the two separate setters.
242
+ */
243
+ let boundService = null;
244
+ export function useCredentials(service) {
245
+ boundService = service || null;
246
+ if (!service) { setCredentialResolver(null); setManagedReader(null); return; }
247
+ setCredentialResolver(name => service.resolve(name));
248
+ setManagedReader(() => readManagedManifest({ credentials: service }));
249
+ }
250
+ /** Unbind `service` only while it is still the bound one, so a late dispose never clears a later re-apply's binding. */
251
+ export function releaseCredentials(service) { if (boundService === service) useCredentials(null); }
252
+ export { resolveRef } from './managed-ref.js';
253
+ // Re-exported so the standalone bundles (memory, search) reach the route through their shared/ closure.
254
+ export { registerManagedConfigRoute } from './managed-config-route.js';
255
+ /**
256
+ * Re-read every key name the config uses and every override name (ENV_OVERRIDES, legacy included) from the
257
+ * DSH credential store (no-op without the store). Values stay in memory and are never logged.
258
+ */
259
+ export async function refreshCredentials(resolved = resolveIntegrations()) {
260
+ if (managedReader) { try { managedManifest = (await managedReader()) ?? null; } catch { managedManifest = null; } }
261
+ if (!credentialResolver) return;
262
+ const resolver = credentialResolver;
263
+ for (const name of new Set([...keyNames(resolved), ...overrideNames(), ...(managedManifest?.entries?.keys() ?? [])])) {
264
+ try {
265
+ const credential = await resolver(name);
266
+ const value = typeof credential === 'string' ? credential : credential?.value;
267
+ if (value) credentialCache.set(name, value); else credentialCache.delete(name);
268
+ } catch { credentialCache.delete(name); }
269
+ }
270
+ }
271
+ /** Every configured key value (store and environment), only for redacting them out of stored text. */
272
+ export function knownSecretValues(resolved = resolveIntegrations()) {
273
+ const env = resolved.env ?? process.env;
274
+ return [...new Set(keyNames(resolved).map(name => lookup([name], resolved, env)).filter(Boolean))];
275
+ }
276
+ /**
277
+ * Whether each section's (and search engine's) key is configured (DSH store or environment), never the value.
278
+ * Each entry also carries `source` (managed-locked | credential | env | managed-default | default) and `locked`.
279
+ */
280
+ export async function keyPresence(resolved) {
281
+ await refreshCredentials(resolved);
282
+ const env = resolved.env ?? process.env, store = storeOf(resolved, env);
283
+ const state = (names, label) => {
284
+ const hit = findKey(names, env, store, resolved.managed);
285
+ return { env: label, present: !!hit, source: hit?.source ?? 'default', locked: names.some(name => resolved.managed?.entries?.get(name)?.locked === true) };
286
+ };
287
+ const out = {};
288
+ for (const section of SECTIONS) {
289
+ const names = sectionKeyNames(section, resolved.effective[section], resolved.managed);
290
+ out[section] = state(names, names[0]);
291
+ }
292
+ out.search.engines = Object.fromEntries(Object.entries(SEARCH_KEY_FIELDS).map(([engine, field]) => {
293
+ const names = sectionKeyNames('search', resolved.effective.search, resolved.managed, field);
294
+ return [engine, state(names, names[0])];
295
+ }));
296
+ return out;
297
+ }
298
+ /** Safe view for the Overview: effective values, the layer each came from, and key presence (never key values). */
299
+ export function describeIntegrations(resolved) {
300
+ return { schemaVersion: 1, revision: resolved.revision, effective: resolved.effective, sources: resolved.sources, keys: resolved.keys, file: resolved.fileLayer, envOverrides: ENV_OVERRIDES };
301
+ }
@@ -0,0 +1,62 @@
1
+ // Validation messages of the integrations resolver, English by default; the host may switch with
2
+ // setIntegrationsLocale(). The zh strings are kept as \u escapes so this source stays ASCII (a user-facing
3
+ // dictionary, not hardcoded UI text).
4
+ const dictionaries = {
5
+ en: {
6
+ notUrl: "is not a valid URL",
7
+ localOrHttps: "must be an https URL (http://127.0.0.1 is allowed for this machine) without credentials or query parameters",
8
+ httpsOnly: "must be an https URL without credentials or query parameters",
9
+ engineList: "must be a non-empty list of {allowed}",
10
+ envName: "must be an environment variable name (upper-case letters, digits, underscores)",
11
+ path: "must be a path starting with /",
12
+ headersObject: "must be a {name: value} object",
13
+ headerForbidden: "cannot set the request header {name} (keys are injected through apiKeyEnv)",
14
+ headerValue: "the value of request header {name} is invalid",
15
+ providerList: "must be a list of channel names",
16
+ noWhitespace: "must be a string without whitespace",
17
+ intRange: "must be an integer from {min} to {max}",
18
+ msRange: "must be {min}-{max} milliseconds",
19
+ byteRange: "must be {min}-{max} bytes",
20
+ oneOf: "must be {values}",
21
+ or: " or ",
22
+ accountName: "must be 1-64 letters, digits or ._@-",
23
+ unsupportedField: "is not a supported field",
24
+ integrationsObject: "integrations must be an object",
25
+ integrationsSection: "integrations does not support {section}",
26
+ integrationsSectionObject: "integrations.{section} must be an object",
27
+ fileCorrupt: "integrations file is corrupt: {reason}",
28
+ },
29
+ zh: {
30
+ notUrl: "\u4e0d\u662f\u5408\u6cd5\u5730\u5740",
31
+ localOrHttps: "\u987b\u4e3a https \u5730\u5740\uff08\u672c\u673a\u53ef\u7528 http://127.0.0.1\uff09\uff0c\u4e0d\u542b\u8d26\u53f7\u548c\u67e5\u8be2\u53c2\u6570",
32
+ httpsOnly: "\u987b\u4e3a\u4e0d\u542b\u8d26\u53f7\u3001\u67e5\u8be2\u53c2\u6570\u7684 https \u5730\u5740",
33
+ engineList: "\u987b\u4e3a\u975e\u7a7a\u5217\u8868\uff0c\u53d6\u503c {allowed}",
34
+ envName: "\u987b\u4e3a\u73af\u5883\u53d8\u91cf\u540d\uff08\u5927\u5199\u5b57\u6bcd\u3001\u6570\u5b57\u3001\u4e0b\u5212\u7ebf\uff09",
35
+ path: "\u987b\u4e3a\u4ee5 / \u5f00\u5934\u7684\u8def\u5f84",
36
+ headersObject: "\u987b\u4e3a {\u540d\u79f0: \u503c} \u5bf9\u8c61",
37
+ headerForbidden: "\u4e0d\u80fd\u8bbe\u7f6e\u8bf7\u6c42\u5934 {name}\uff08\u5bc6\u94a5\u901a\u8fc7 apiKeyEnv \u6ce8\u5165\uff09",
38
+ headerValue: "\u8bf7\u6c42\u5934 {name} \u7684\u503c\u65e0\u6548",
39
+ providerList: "\u987b\u4e3a\u6e20\u9053\u540d\u5217\u8868",
40
+ noWhitespace: "\u987b\u4e3a\u4e0d\u542b\u7a7a\u767d\u7684\u5b57\u7b26\u4e32",
41
+ intRange: "\u987b\u4e3a {min}\u2013{max} \u7684\u6574\u6570",
42
+ msRange: "\u987b\u4e3a {min}\u2013{max} \u6beb\u79d2",
43
+ byteRange: "\u987b\u4e3a {min}\u2013{max} \u5b57\u8282",
44
+ oneOf: "\u987b\u4e3a {values}",
45
+ or: " \u6216 ",
46
+ accountName: "\u987b\u4e3a 1\u201364 \u4f4d\u5b57\u6bcd\u3001\u6570\u5b57\u6216 ._@-",
47
+ unsupportedField: "\u4e0d\u662f\u652f\u6301\u7684\u5b57\u6bb5",
48
+ integrationsObject: "integrations \u987b\u4e3a\u5bf9\u8c61",
49
+ integrationsSection: "integrations \u4e0d\u652f\u6301 {section}",
50
+ integrationsSectionObject: "integrations.{section} \u987b\u4e3a\u5bf9\u8c61",
51
+ fileCorrupt: "\u96c6\u6210\u914d\u7f6e\u6587\u4ef6\u635f\u574f\uff1a{reason}",
52
+ },
53
+ };
54
+ let locale = 'en';
55
+ /** 'zh', 'zh-CN', ... select Chinese; anything else English. */
56
+ export function setIntegrationsLocale(next) { locale = String(next ?? '').toLowerCase().startsWith('zh') ? 'zh' : 'en'; }
57
+ export const integrationsLocale = () => locale;
58
+ /** The message for `key` with `{name}` placeholders filled from `values`. */
59
+ export function message(key, values = {}) {
60
+ const text = dictionaries[locale]?.[key] ?? dictionaries.en[key] ?? key;
61
+ return text.replace(/\{(\w+)\}/g, (whole, name) => (name in values ? String(values[name]) : whole));
62
+ }
@@ -0,0 +1,17 @@
1
+ import { m as modelText } from './i18n.js';
2
+ import { executeFreeTask } from './task-bridge.js';
3
+ import { params } from './tool-schema.js';
4
+ const output = { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: value }] };
5
+ export function apply(ctx, team) {
6
+ // CPA tools are supplied by the independent profile bundle; this plugin owns only the free-task bridge.
7
+ ctx.effect(() => ctx.tools.register({
8
+ name: 'capability_free_task',
9
+ description: modelText('integrations.262'),
10
+ parameters: params({ prompt: { type: 'string', description: modelText('integrations.263') }, subject: { type: 'string', description: modelText('integrations.264') } }, ['prompt']), output,
11
+ async execute(args, exec) {
12
+ const cwd = await ctx.workingDirectory.ensure(exec.agent, exec.signal);
13
+ const { effective } = await team.load(exec.agent);
14
+ return JSON.stringify(await executeFreeTask({ teams: ctx.agentTeams, agent: exec.agent, prompt: args.prompt, subject: args.subject, cwd, signal: exec.signal, tools: effective.workflow.freeTools, models: effective.workflow.freeModels }));
15
+ },
16
+ }));
17
+ }
package/lead-route.js ADDED
@@ -0,0 +1,30 @@
1
+ // The Lead follows the model the user picked in the conversation. DSH puts that pick on the request config it
2
+ // passes to agent/request; this plugin used to overwrite it with the configured lead route. Teammates keep strict
3
+ // per-role routing. Same-tier failover still applies: configured lead routes in the chosen model's tier stay
4
+ // as fallbacks, and a model outside that tier gets none, so a failure never degrades across tiers.
5
+ import { routeKey, routeMatcher } from './model-policy.js';
6
+ /** The user's pick from DSH's request config, or null when the request names no model. */
7
+ export function userChoice(previous) {
8
+ const { provider, model } = previous ?? {};
9
+ return typeof provider === 'string' && provider && typeof model === 'string' && model ? { provider, model } : null;
10
+ }
11
+ /**
12
+ * The Lead role re-pointed at the user's pick. `tierModels` lists the model ids of the configured lead tier;
13
+ * `disabledRoutes` is the profile-wide model policy. A pick the policy disables keeps the configured route.
14
+ */
15
+ export function followUserChoice(role, previous, { tierModels = [], disabledRoutes = [] } = {}) {
16
+ const choice = userChoice(previous);
17
+ if (!choice) return role;
18
+ if (routeMatcher(disabledRoutes)(routeKey(choice))) return { ...role, userChoiceRejected: routeKey(choice) };
19
+ const configured = role.unassigned || !role.provider ? [] : [{ provider: role.provider, model: role.model }, ...(role.fallbacks ?? [])];
20
+ const same = r => routeKey(r) === routeKey(choice);
21
+ const sameTier = configured.some(same) || tierModels.includes(choice.model);
22
+ // The long-context switch is a configured-route feature; the user's explicit pick is not swapped out.
23
+ const { longContext: _longContext, unassigned: _unassigned, ...rest } = role;
24
+ return { ...rest, ...choice, fallbacks: sameTier ? configured.filter(r => !same(r)) : [], userChoice: true };
25
+ }
26
+ /** The outgoing request: the user's own settings survive when the user's pick is the route actually used. */
27
+ export function leadRequest(previous, selected) {
28
+ const choice = userChoice(previous);
29
+ return choice && routeKey(choice) === routeKey(selected) ? { ...previous, provider: selected.provider, model: selected.model } : null;
30
+ }
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "AIWUK Team",
4
+ "description": "Capability-routed multi-agent team for DeepSeek Harness: per-role model routing, same-tier failover and conversational team configuration."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "AI悟空 协作团队",
4
+ "description": "为 DeepSeek Harness 提供按能力路由的多智能体团队:按角色分配模型、同档容灾、对话式配置团队。"
5
+ }
6
+ }
@@ -0,0 +1,30 @@
1
+ // Same-tier long-context route: once the next prompt of a member's session reaches its capability's threshold,
2
+ // requests go to the configured long-context model first. They return only after the prompt falls below 80% of
3
+ // the threshold (for example after compaction), so a session does not flip models, and caches, on every step.
4
+ const RELEASE = 0.8;
5
+ /** Next-prompt size of a session from token-meter's `contextPressure` state, as its wire view computes it. */
6
+ export function promptTokens(projections, session) {
7
+ try {
8
+ const state = projections?.stateOf(session, 'contextPressure');
9
+ if (!state) return undefined;
10
+ if (state.pressureTokens !== undefined && state.sampledSurfaceTokens !== undefined) return Math.max(0, state.pressureTokens + state.surfaceTokens - state.sampledSurfaceTokens);
11
+ return state.surfaceTokens; // No provider sample yet: the heuristic surface is the only estimate.
12
+ } catch { return undefined; }
13
+ }
14
+ export function createLongContext() {
15
+ const active = new WeakMap(); // session → whether the long-context route is engaged
16
+ return {
17
+ reset(session) { active.delete(session); },
18
+ /** The role with its long-context route first while the session's prompt is large; the role itself otherwise. */
19
+ apply(session, role, tokens) {
20
+ const lc = role.longContext;
21
+ if (!lc || tokens === undefined) return role;
22
+ const on = tokens >= lc.thresholdTokens * (active.get(session) ? RELEASE : 1);
23
+ active.set(session, on);
24
+ if (!on) return role;
25
+ const same = r => r.provider === lc.provider && r.model === lc.model;
26
+ const rest = [{ provider: role.provider, model: role.model }, ...(role.fallbacks ?? [])].filter(r => !same(r));
27
+ return { ...role, provider: lc.provider, model: lc.model, fallbacks: rest, longContextActive: true };
28
+ },
29
+ };
30
+ }