@wowyuarm/dsh-agent-team 0.1.15 → 0.2.1

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 (99) hide show
  1. package/README.md +8 -4
  2. package/README.zh.md +8 -4
  3. package/cordis.patch.yml +26 -12
  4. package/package.json +52 -47
  5. package/packages/agent-team/lib/attachments.js +63 -4
  6. package/packages/agent-team/lib/compaction-engine.js +18 -0
  7. package/packages/agent-team/lib/context-continuity-host.js +198 -4
  8. package/packages/agent-team/lib/context-judge.js +150 -0
  9. package/packages/agent-team/lib/context-source.js +11 -6
  10. package/packages/agent-team/lib/environment-check.js +102 -0
  11. package/packages/agent-team/lib/human-profile.js +23 -37
  12. package/packages/agent-team/lib/index.js +510 -99
  13. package/packages/agent-team/lib/installed-manifest.js +32 -0
  14. package/packages/agent-team/lib/invariant.js +36 -12
  15. package/packages/agent-team/lib/ledger.js +37 -1
  16. package/packages/agent-team/lib/member-context.js +2 -4
  17. package/packages/agent-team/lib/member-runtime.js +2 -1
  18. package/packages/agent-team/lib/member-time-context.js +2 -4
  19. package/packages/agent-team/lib/recovery.js +53 -4
  20. package/packages/agent-team/lib/typert.host.js +225 -54
  21. package/packages/agent-team/lib/typert.remote-client.d.ts +5 -1
  22. package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
  23. package/packages/agent-team/lib/typert.remote-client.js +127 -39
  24. package/packages/agent-team/lib/types/attachments.d.ts +32 -2
  25. package/packages/agent-team/lib/types/attachments.d.ts.map +1 -1
  26. package/packages/agent-team/lib/types/compaction-engine.d.ts +19 -0
  27. package/packages/agent-team/lib/types/compaction-engine.d.ts.map +1 -0
  28. package/packages/agent-team/lib/types/context-continuity-host.d.ts +131 -4
  29. package/packages/agent-team/lib/types/context-continuity-host.d.ts.map +1 -1
  30. package/packages/agent-team/lib/types/context-judge.d.ts +82 -0
  31. package/packages/agent-team/lib/types/context-judge.d.ts.map +1 -0
  32. package/packages/agent-team/lib/types/context-source.d.ts +7 -4
  33. package/packages/agent-team/lib/types/context-source.d.ts.map +1 -1
  34. package/packages/agent-team/lib/types/environment-check.d.ts +66 -0
  35. package/packages/agent-team/lib/types/environment-check.d.ts.map +1 -0
  36. package/packages/agent-team/lib/types/human-profile.d.ts +21 -27
  37. package/packages/agent-team/lib/types/human-profile.d.ts.map +1 -1
  38. package/packages/agent-team/lib/types/index.d.ts +281 -18
  39. package/packages/agent-team/lib/types/index.d.ts.map +1 -1
  40. package/packages/agent-team/lib/types/installed-manifest.d.ts +16 -0
  41. package/packages/agent-team/lib/types/installed-manifest.d.ts.map +1 -0
  42. package/packages/agent-team/lib/types/invariant.d.ts +27 -5
  43. package/packages/agent-team/lib/types/invariant.d.ts.map +1 -1
  44. package/packages/agent-team/lib/types/ledger.d.ts +12 -0
  45. package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
  46. package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
  47. package/packages/agent-team/lib/types/member-runtime.d.ts +1 -1
  48. package/packages/agent-team/lib/types/member-runtime.d.ts.map +1 -1
  49. package/packages/agent-team/lib/types/member-time-context.d.ts.map +1 -1
  50. package/packages/agent-team/lib/types/recovery.d.ts +19 -6
  51. package/packages/agent-team/lib/types/recovery.d.ts.map +1 -1
  52. package/packages/agent-team/lib/types/types/entities.d.ts +1 -8
  53. package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
  54. package/packages/agent-team/lib/types/types/requests-results.d.ts +94 -2
  55. package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
  56. package/packages/agent-team/lib/types/types.d.ts +9 -0
  57. package/packages/agent-team/lib/types/types.d.ts.map +1 -1
  58. package/packages/client-agent-team/lib/client.js +1264 -383
  59. package/packages/client-agent-team/lib/client.js.map +1 -1
  60. package/packages/client-agent-team/lib/types/client/EnvironmentCheck.d.ts +18 -0
  61. package/packages/client-agent-team/lib/types/client/EnvironmentCheck.d.ts.map +1 -0
  62. package/packages/client-agent-team/lib/types/client/EnvironmentCheck.js +58 -0
  63. package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +0 -5
  64. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts +1 -1
  65. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts.map +1 -1
  66. package/packages/client-agent-team/lib/types/client/TeamPresenceDot.js +2 -7
  67. package/packages/client-agent-team/lib/types/client/TeamSettingsSection.d.ts +26 -0
  68. package/packages/client-agent-team/lib/types/client/TeamSettingsSection.d.ts.map +1 -0
  69. package/packages/client-agent-team/lib/types/client/TeamSettingsSection.js +195 -0
  70. package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts +7 -11
  71. package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts.map +1 -1
  72. package/packages/client-agent-team/lib/types/client/attachment-preview.js +13 -3
  73. package/packages/client-agent-team/lib/types/client/context-judge.d.ts +64 -0
  74. package/packages/client-agent-team/lib/types/client/context-judge.d.ts.map +1 -0
  75. package/packages/client-agent-team/lib/types/client/context-judge.js +78 -0
  76. package/packages/client-agent-team/lib/types/client/environment-check.d.ts +65 -0
  77. package/packages/client-agent-team/lib/types/client/environment-check.d.ts.map +1 -0
  78. package/packages/client-agent-team/lib/types/client/environment-check.js +78 -0
  79. package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
  80. package/packages/client-agent-team/lib/types/client/index.js +84 -11
  81. package/packages/client-agent-team/lib/types/client/judge-form.d.ts +99 -0
  82. package/packages/client-agent-team/lib/types/client/judge-form.d.ts.map +1 -0
  83. package/packages/client-agent-team/lib/types/client/judge-form.js +208 -0
  84. package/packages/client-agent-team/lib/types/client/locales.d.ts +70 -14
  85. package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
  86. package/packages/client-agent-team/lib/types/client/locales.js +76 -14
  87. package/packages/tool-agent-team/README.md +6 -3
  88. package/packages/tool-agent-team/README.zh.md +6 -3
  89. package/packages/tool-agent-team/lib/context-tools.js +148 -164
  90. package/packages/tool-agent-team/lib/index.js +17 -2
  91. package/packages/tool-agent-team/lib/types/context-tools.d.ts +32 -7
  92. package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -1
  93. package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
  94. package/packages/agent-team/lib/pressure-policy.js +0 -230
  95. package/packages/agent-team/lib/types/pressure-policy.d.ts +0 -130
  96. package/packages/agent-team/lib/types/pressure-policy.d.ts.map +0 -1
  97. package/packages/client-agent-team/lib/types/client/HumanSettingsSection.d.ts +0 -15
  98. package/packages/client-agent-team/lib/types/client/HumanSettingsSection.d.ts.map +0 -1
  99. package/packages/client-agent-team/lib/types/client/HumanSettingsSection.js +0 -115
@@ -6,9 +6,11 @@
6
6
  * Team notices, and how a rollover's durable identity is named).
7
7
  *
8
8
  * The engine owns every mechanic this module does not state: the idle-boundary
9
- * swap, carried input, checkpoint continuations, crash repair. Team owns the
10
- * Member vocabulary and the ledger-backed lifecycle behind
11
- * {@link TeamContextContinuityOptions.executeTransition}.
9
+ * swap, carried input, checkpoint continuations, crash repair, and the
10
+ * context-pressure policy itself. Team owns the Member vocabulary and the
11
+ * ledger-backed lifecycle behind
12
+ * {@link TeamContextContinuityOptions.executeTransition}, and the per-Member
13
+ * readings the pressure policy asks for.
12
14
  *
13
15
  * The projection state this host hands the coordinator is the engine's own
14
16
  * (`ContextProjectionState`), read from the unit `context-projection.ts`
@@ -16,8 +18,9 @@
16
18
  * @module @wowyuarm/dsh-agent-team/context-continuity-host
17
19
  */
18
20
  import { createHash } from 'node:crypto';
19
- import { ContextContinuityCoordinator, ContextMessageCodec, } from '@wowyuarm/dsh-context-continuity';
21
+ import { ContextContinuityCoordinator, ContextMessageCodec, ContextPressurePolicy, } from '@wowyuarm/dsh-context-continuity';
20
22
  import { SessionId as SessionIdBrand } from '@deepseek-ai/dsh-session';
23
+ import z from '@deepseek-ai/schemastery';
21
24
  import { AGENT_TEAM_PLUGIN_ID, isAgentTeamSource } from "./context-source.js";
22
25
  /**
23
26
  * The one writer of context-continuity messages for Team. Section names are
@@ -88,3 +91,194 @@ export class TeamContextContinuityHost {
88
91
  export function createTeamContextManagement(options) {
89
92
  return new ContextContinuityCoordinator(new TeamContextContinuityHost(options), TEAM_CONTEXT_CODEC);
90
93
  }
94
+ /**
95
+ * Team's wording for the engine's pressure notice: the two labels name work in
96
+ * Team's own vocabulary, while the numbers, the ordering and the default action
97
+ * stay the engine's. A label is presentation; the notice's substance is not a
98
+ * knob.
99
+ */
100
+ const TEAM_PRESSURE_TEXT = {
101
+ inHandLabel: 'Active Claims',
102
+ jobsLabel: 'Owner jobs',
103
+ };
104
+ /** How many of the generation's earlier user inputs one judgement is given. */
105
+ const RECENT_INPUT_LIMIT = 8;
106
+ /**
107
+ * The long-gap gate as row configuration: every field is optional, so a
108
+ * deployment states only the thresholds it disagrees with and the engine's own
109
+ * default answers the rest. The three answer different questions — how large a
110
+ * context the gate applies to, what counts as a long gap, and how long one
111
+ * judgement may take before the step proceeds without one — and the last is the
112
+ * one that decides whether a configured judge is ever heard, because a judge
113
+ * whose own budget is longer than this deadline never answers in time.
114
+ *
115
+ * All three are volatile, and the engine is what makes that safe: it reads
116
+ * every field at the step it applies to, never at construction, so a Host that
117
+ * keeps one gate object and refreshes its fields in place changes the policy a
118
+ * running generation is judged by. `judgeTimeoutMs` is the field that needs it
119
+ * most — the deadline a judge can answer within is a property of the endpoint,
120
+ * not of the bundle, and a deployment that discovers its endpoint is slow must
121
+ * be able to raise it without editing a patch layer and restarting.
122
+ */
123
+ export const TEAM_PRESSURE_GATE_SCHEMA = z.object({
124
+ tokens: z.natural().volatile().description('Context size at or above which the long-gap gate may hold a step. Default: 128000.'),
125
+ idleMs: z.natural().volatile().description('How long a generation must have been idle for a step to count as a long gap. Default: 1800000 (30 minutes).'),
126
+ judgeTimeoutMs: z.natural().volatile().description("How long one relatedness judgement may take before the step proceeds without one. Default: 10000, not the engine's own 5000."),
127
+ });
128
+ /**
129
+ * Team's `PressurePolicyHost`. Every member is resolved per Member at call
130
+ * time, because one policy serves every Member this Host runs. The readings
131
+ * that need the step's own argument are fed for exactly one policy call —
132
+ * {@link beginStep} to {@link endStep} — so a judgement is never made from a
133
+ * neighbour Member's messages.
134
+ */
135
+ export class TeamPressurePolicyHost {
136
+ options;
137
+ pluginId = AGENT_TEAM_PLUGIN_ID;
138
+ admitting = new Map();
139
+ constructor(options) {
140
+ this.options = options;
141
+ }
142
+ /** Record the input one step is admitting: the host's own pre-step argument. */
143
+ beginStep(memberId, messages) {
144
+ this.admitting.set(memberId, messages);
145
+ }
146
+ /** Forget that input again: the view lives only while its policy call runs. */
147
+ endStep(memberId) {
148
+ this.admitting.delete(memberId);
149
+ }
150
+ limitsFor(memberId) {
151
+ const agent = this.options.agentForMember(memberId);
152
+ return agent === undefined ? undefined : this.options.limitsForAgent(agent);
153
+ }
154
+ /**
155
+ * The Member's durable surface, for proving a reduction. Without a live Agent
156
+ * there is nothing to measure; the policy reaches this only for a Member whose
157
+ * limits resolved, which already requires one.
158
+ */
159
+ surfaceFor(memberId) {
160
+ const agent = this.options.agentForMember(memberId);
161
+ if (agent === undefined)
162
+ return { generation: 0 };
163
+ const tokens = agent.ctx.get('tokenMeter')?.measure(agent.session)?.totalTokens;
164
+ return { generation: agent.session.surface.replaceGeneration, ...(tokens === undefined ? {} : { tokens }) };
165
+ }
166
+ compactionFor(memberId) {
167
+ const agent = this.options.agentForMember(memberId);
168
+ return agent === undefined ? undefined : this.options.compactionForAgent(agent);
169
+ }
170
+ /**
171
+ * The Member's durable log, as the notice latch and the idle gap read it. The
172
+ * engine's contract asks for the span's own events and the inherited count
173
+ * beside them; the `ownEvents()` reader is what states that boundary today.
174
+ */
175
+ logSpanFor(memberId) {
176
+ const agent = this.options.agentForMember(memberId);
177
+ if (agent === undefined)
178
+ return { sessionId: '', inheritedEventCount: 0, events: [] };
179
+ return { sessionId: agent.session.id, inheritedEventCount: agent.session.inheritedEventCount, events: agent.session.ownEvents() };
180
+ }
181
+ inHandFor(memberId) {
182
+ return {
183
+ inHand: this.options.activeClaimLabels(memberId),
184
+ jobs: this.options.runningJobLabels(memberId),
185
+ };
186
+ }
187
+ /**
188
+ * The input this step is admitting, and the generation's earlier user input
189
+ * beside it. Both sides exclude Team's own notices: what the gate judges is
190
+ * what a human sent, and a rederived Team notification is neither the request
191
+ * being held nor evidence of what the request continues.
192
+ */
193
+ relatednessFor(memberId) {
194
+ const agent = this.options.agentForMember(memberId);
195
+ const admitted = this.admitting.get(memberId);
196
+ if (agent === undefined || admitted === undefined)
197
+ return undefined;
198
+ const input = externalInputText(admitted);
199
+ if (input.trim().length === 0)
200
+ return undefined;
201
+ return { input, recent: this.recentInput(agent, admitted) };
202
+ }
203
+ /**
204
+ * The generation's earlier human input, oldest first, without the messages
205
+ * this step is admitting: the judge is asked what the arriving request
206
+ * continues, so the arriving request is not also part of its own evidence.
207
+ */
208
+ recentInput(agent, admitted) {
209
+ const admittedIds = new Set(admitted.map(message => message.id));
210
+ return agent.session.deriveMessages()
211
+ .flatMap(message => message.role === 'user' && !admittedIds.has(message.id) && !isAgentTeamSource(message.source)
212
+ ? [messageText(message)]
213
+ : [])
214
+ .filter(text => text.trim().length > 0)
215
+ .slice(-RECENT_INPUT_LIMIT);
216
+ }
217
+ judgeFor(memberId) {
218
+ const resolve = this.options.judgeForAgent;
219
+ const agent = this.options.agentForMember(memberId);
220
+ if (resolve === undefined || agent === undefined)
221
+ return undefined;
222
+ return resolve(agent);
223
+ }
224
+ /**
225
+ * Steer one notice into the Member's running turn. A Member with no live
226
+ * Agent throws rather than reporting success: the caller holds a step on the
227
+ * strength of this call, and a held step whose instruction never arrived
228
+ * would stall the Member behind a message that does not exist.
229
+ */
230
+ steer(memberId, notice) {
231
+ const agent = this.options.agentForMember(memberId);
232
+ if (agent === undefined)
233
+ throw new Error('the Member has no live Agent to steer the notice into');
234
+ agent.steer(notice);
235
+ }
236
+ failedFor(memberId, diagnostic) {
237
+ this.options.failed(memberId, diagnostic);
238
+ }
239
+ log(message, memberId) {
240
+ this.options.log(message, memberId);
241
+ }
242
+ }
243
+ /**
244
+ * The engine's context-pressure policy over Team's Member lifecycle: the one
245
+ * place the two sides meet. It owns no decision of its own — the ordering, the
246
+ * notice latch, the fail-closed reduction proof and the long-gap gate are all
247
+ * the engine's — and adds only the step-scoped input view that only the host
248
+ * can have.
249
+ */
250
+ export class TeamPressurePolicy {
251
+ host;
252
+ policy;
253
+ constructor(options) {
254
+ this.host = new TeamPressurePolicyHost(options);
255
+ this.policy = new ContextPressurePolicy(this.host, TEAM_PRESSURE_TEXT, options.gate);
256
+ }
257
+ /** The pre-step decision for one Member, with the input that step claims. */
258
+ async onPreStep(memberId, messages, signal) {
259
+ this.host.beginStep(memberId, messages);
260
+ try {
261
+ return await this.policy.onPreStep(memberId, signal);
262
+ }
263
+ finally {
264
+ this.host.endStep(memberId);
265
+ }
266
+ }
267
+ onRequestError(memberId, failure, signal) {
268
+ return this.policy.onRequestError(memberId, failure, signal);
269
+ }
270
+ onAssistantMessage(memberId) {
271
+ this.policy.onAssistantMessage(memberId);
272
+ }
273
+ dispose() {
274
+ this.policy.dispose();
275
+ }
276
+ }
277
+ /** The text a step is admitting that came from outside Team, in arrival order. */
278
+ function externalInputText(messages) {
279
+ return messages.flatMap(message => isAgentTeamSource(message.source) ? [] : [messageText(message)]).join('\n');
280
+ }
281
+ /** One message's visible text, blocks in order. */
282
+ function messageText(message) {
283
+ return message.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('\n');
284
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Team's relatedness judge: the long-gap gate's `jev` service, mounted by this
3
+ * bundle when a key resolves and left unmounted when none does.
4
+ *
5
+ * The key is the switch, so nothing here decides policy: a key in the Host
6
+ * row's `jev` config — or in the environment variable that config names —
7
+ * mounts the service, clearing it unmounts the service, and the gate asks
8
+ * whatever is mounted. A deployment that mounts its own `jev` row keeps
9
+ * working, because the judge is read as a service rather than as this mount.
10
+ *
11
+ * The mount also carries the one setting the two sides must agree on: jev's own
12
+ * call budget is derived from the gate's deadline, so a judge can never be
13
+ * configured to outlive the time the gate gives it — the mismatch that would
14
+ * otherwise leave every long-gap step ungated without a word.
15
+ * @module @wowyuarm/dsh-agent-team/context-judge
16
+ */
17
+ import { DEFAULT_API_KEY_ENV, Jev } from '@wowyuarm/dsh-jev';
18
+ /**
19
+ * The judge's call budget keeps this much of the gate's deadline for the hop
20
+ * back: the gate abandons a judgement that outlives its own deadline, so a
21
+ * judge allowed to use the whole deadline would always be abandoned first.
22
+ */
23
+ const JUDGE_DEADLINE_MARGIN_MS = 1_000;
24
+ /**
25
+ * Wait before the judge's second attempt. Pinned rather than inherited from the
26
+ * service's own default, because it is half of the same budget the deadline
27
+ * above sizes: a backoff longer than this would spend the gate's remaining time
28
+ * waiting instead of asking again.
29
+ */
30
+ const JUDGE_RETRY_DELAY_MS = 500;
31
+ /** The key a row states, or the one the named environment variable holds. */
32
+ function resolvedKey(config, envName) {
33
+ const stated = config?.apiKey;
34
+ if (stated !== undefined && stated !== '')
35
+ return stated;
36
+ const fromEnv = process.env[envName];
37
+ return fromEnv === undefined || fromEnv === '' ? undefined : fromEnv;
38
+ }
39
+ /**
40
+ * The config one mount runs with. Exported because it is the whole contract
41
+ * between the gate's deadline and the judge's own budget, and a pure function
42
+ * is what a test can hold still.
43
+ */
44
+ export function judgeMountConfig(config, key, envName, deadlineMs) {
45
+ return {
46
+ apiKey: key,
47
+ apiKeyEnv: envName,
48
+ ...(config?.model === undefined ? {} : { model: config.model }),
49
+ ...(config?.apiBase === undefined ? {} : { apiBase: config.apiBase }),
50
+ timeoutMs: Math.max(JUDGE_DEADLINE_MARGIN_MS, deadlineMs - JUDGE_DEADLINE_MARGIN_MS),
51
+ // Two attempts: this call sits on the path that starts a turn, so the
52
+ // budget stays small — but one transport blip should not cost the step its
53
+ // gate, which is what a single attempt did. The gate races the whole call
54
+ // against its deadline anyway, so a second attempt that cannot finish in
55
+ // time costs the turn nothing beyond what it already spent.
56
+ attempts: 2,
57
+ retryDelayMs: JUDGE_RETRY_DELAY_MS,
58
+ };
59
+ }
60
+ /**
61
+ * One Host's judge: the service it mounted, the failure that kept one from
62
+ * mounting, and the reconciliation that keeps the two in step with the row.
63
+ */
64
+ export class TeamContextJudge {
65
+ ctx;
66
+ options;
67
+ fiber;
68
+ failure;
69
+ /** The mount the live fiber runs with, so an unchanged row does not restart it. */
70
+ mounted;
71
+ constructor(ctx, options) {
72
+ this.ctx = ctx;
73
+ this.options = options;
74
+ }
75
+ /**
76
+ * The judge a step may ask, or undefined when the deployment has none: this
77
+ * bundle's own mount first, then a service another row mounted.
78
+ */
79
+ judge() {
80
+ return this.fiber?.ctx.get('jev') ?? this.ctx.get('jev');
81
+ }
82
+ /** The environment variable this deployment reads the key from. */
83
+ keyEnv() {
84
+ const name = this.options.config()?.apiKeyEnv;
85
+ return name === undefined || name === '' ? DEFAULT_API_KEY_ENV : name;
86
+ }
87
+ /** Whether a key is resolvable right now, from the row or from its environment. */
88
+ key() {
89
+ return resolvedKey(this.options.config(), this.keyEnv());
90
+ }
91
+ /**
92
+ * Make the mount match the row: mount when a key resolves and none is
93
+ * mounted, re-configure a live mount whose settings changed, unmount when the
94
+ * key is gone. Called at init and on every volatile update of the row, so a
95
+ * key written in the Team settings surface takes effect without a restart.
96
+ */
97
+ async sync() {
98
+ const config = this.options.config();
99
+ const key = this.key();
100
+ if (key === undefined) {
101
+ this.failure = undefined;
102
+ await this.unmount();
103
+ return;
104
+ }
105
+ const wanted = judgeMountConfig(config, key, this.keyEnv(), this.options.deadlineMs());
106
+ const signature = JSON.stringify(wanted);
107
+ if (this.fiber !== undefined && signature === this.mounted)
108
+ return;
109
+ if (this.fiber === undefined) {
110
+ this.fiber = this.ctx.plugin(Jev, wanted);
111
+ this.mounted = signature;
112
+ }
113
+ else {
114
+ this.mounted = signature;
115
+ // `update` re-activates the fiber with the new config, and clears a
116
+ // previous failure, so a corrected endpoint recovers without a restart.
117
+ this.fiber.update(wanted);
118
+ }
119
+ try {
120
+ await this.fiber.await();
121
+ this.failure = undefined;
122
+ }
123
+ catch (error) {
124
+ // A rejected key or endpoint is a configuration failure the settings
125
+ // surface reports: the row keeps what the Human wrote, and the status
126
+ // says why the judge is not answering.
127
+ this.failure = error instanceof Error ? error.message : String(error);
128
+ }
129
+ }
130
+ /** Drop this bundle's mount, if it has one. */
131
+ async unmount() {
132
+ const fiber = this.fiber;
133
+ this.fiber = undefined;
134
+ this.mounted = undefined;
135
+ if (fiber !== undefined)
136
+ await fiber.dispose();
137
+ }
138
+ /** What the Team settings surface states about the judge. */
139
+ status() {
140
+ const keyEnv = this.keyEnv();
141
+ if (this.judge() !== undefined)
142
+ return { enabled: true, keyEnv };
143
+ return {
144
+ enabled: false,
145
+ reason: this.key() === undefined ? 'no-key' : 'unavailable',
146
+ ...(this.failure === undefined ? {} : { detail: this.failure }),
147
+ keyEnv,
148
+ };
149
+ }
150
+ }
@@ -34,19 +34,24 @@ export const AGENT_TEAM_V3_RENAMED_KIND = `plugin:${AGENT_TEAM_PLUGIN_ID}`;
34
34
  /** The kind the read-time conversion renames one producer's released V3 sources into. */
35
35
  export const v3RenamedSourceKind = (producer) => `plugin:${producer}`;
36
36
  /**
37
- * Whether one message source carries this plugin's own attribution. Both
38
- * identities are matched by exact kind equality — never by a `plugin:`
39
- * prefix test, which would claim third-party producers' rows as Team facts.
37
+ * Whether one source kind is a producer's own: the kind it writes now and the
38
+ * read-time conversion of its released V3 history. Both identities are matched
39
+ * by exact kind equality — never by a `plugin:` prefix test, which would claim
40
+ * third-party producers' rows as that producer's facts.
40
41
  */
42
+ export function matchesProducerKind(kind, producer) {
43
+ return kind === producer || kind === v3RenamedSourceKind(producer);
44
+ }
45
+ /** Whether one message source carries this plugin's own attribution. */
41
46
  export function isAgentTeamSource(source) {
42
- return source.kind === AGENT_TEAM_PLUGIN_ID || source.kind === AGENT_TEAM_V3_RENAMED_KIND;
47
+ return matchesProducerKind(source.kind, AGENT_TEAM_PLUGIN_ID);
43
48
  }
44
49
  /**
45
50
  * Whether one source kind carries this plugin's attribution, for call sites
46
- * whose sources arrive untyped. Exact identities only, never a prefix test.
51
+ * whose sources arrive untyped.
47
52
  */
48
53
  export function isAgentTeamSourceKind(kind) {
49
- return kind === AGENT_TEAM_PLUGIN_ID || kind === AGENT_TEAM_V3_RENAMED_KIND;
54
+ return matchesProducerKind(kind, AGENT_TEAM_PLUGIN_ID);
50
55
  }
51
56
  /** Handoff snapshot section name carrying the model-authored prose. */
52
57
  export const HANDOFF_SECTION_NAME = 'HANDOFF';
@@ -0,0 +1,102 @@
1
+ /** The one local environment check: how the running DSH compares to the line this bundle declares. */
2
+ import { evaluatePluginCompatibility, getDshRuntimeVersion } from '@deepseek-ai/dsh-app-boot';
3
+ import { bundleVersionOf, readInstalledManifest } from "./installed-manifest.js";
4
+ /** Prefix of every peer that sits on the DSH version line; the bare scope is not one. */
5
+ const DSH_PEER_PREFIX = '@deepseek-ai/dsh-';
6
+ /** The `>=lower <upper>` shape every DSH peer carries; a comparator series, not a single version. */
7
+ const SUPPORT_RANGE_SHAPE = /^>=(\S+)\s+<(\S+)$/u;
8
+ /**
9
+ * Read the support line from the installed manifest's DSH peers, or nothing.
10
+ *
11
+ * Every `@deepseek-ai/dsh-*` peer must carry exactly the same range, and it must
12
+ * be a `>=<lower> <upper>` series: the version line is the manifest's own claim
13
+ * and a partially drifted manifest cannot state one. `scripts/check-version-consistency.mjs`
14
+ * holds the same shape to the certified baseline, so a run of this bundle
15
+ * states the same line its README does. Returns `undefined` when the peers
16
+ * disagree or the shape is unreadable.
17
+ */
18
+ export function supportRangeOf(manifest) {
19
+ const fields = manifest;
20
+ if (typeof fields.peerDependencies !== 'object' || fields.peerDependencies === null)
21
+ return undefined;
22
+ const ranges = new Set();
23
+ for (const [name, range] of Object.entries(fields.peerDependencies)) {
24
+ if (!name.startsWith(DSH_PEER_PREFIX))
25
+ continue;
26
+ if (typeof range !== 'string')
27
+ return undefined;
28
+ ranges.add(range);
29
+ }
30
+ const distinct = [...ranges];
31
+ if (distinct.length !== 1)
32
+ return undefined;
33
+ const shape = SUPPORT_RANGE_SHAPE.exec(distinct[0]);
34
+ if (shape === null)
35
+ return undefined;
36
+ return Object.freeze({ lower: shape[1], upper: shape[2] });
37
+ }
38
+ /**
39
+ * Judge one already-resolved triple, so every branch is testable without an
40
+ * environment. The verdict comes from the Harness's own
41
+ * `evaluatePluginCompatibility` — the plugin manager decides installability with
42
+ * that same function, and prerelease ordering is exactly what a hand-written
43
+ * comparison gets wrong.
44
+ *
45
+ * @param manifest - the installed bundle's parsed manifest, the source of the declared support line.
46
+ * @param runtimeVersion - the running DSH version, or `'unknown'` when it could not be read.
47
+ * @param bundleVersion - the installed bundle version, or `'unknown'`.
48
+ * @param unavailable - why the running version is not known, recorded when it is `'unknown'`.
49
+ * @returns the report the settings page renders.
50
+ */
51
+ export function evaluateEnvironment(manifest, runtimeVersion, bundleVersion, unavailable) {
52
+ const range = supportRangeOf(manifest);
53
+ const base = {
54
+ bundleVersion,
55
+ ...(range === undefined ? {} : { certifiedDshVersion: range.lower, supportRange: range }),
56
+ };
57
+ if (runtimeVersion === 'unknown') {
58
+ return Object.freeze({ ...base, verdict: 'undetermined', reason: unavailable ?? 'the running dsh version is unavailable' });
59
+ }
60
+ const running = { ...base, dshVersion: runtimeVersion };
61
+ // A real violation is answered first, and it is never downgraded to
62
+ // `undetermined`: a peer that drifted to a range of its own is a genuine
63
+ // "not satisfied" even though no single range can then be stated for the
64
+ // page. Only when nothing is violated does the question become whether the
65
+ // declared line can be stated at all.
66
+ // One evaluation covers every dsh peer in the manifest, and `range` above is
67
+ // derived from exactly those peers: re-judging the range it prints could not
68
+ // change this answer.
69
+ const satisfied = evaluatePluginCompatibility(manifest, {}, runtimeVersion) === undefined;
70
+ if (!satisfied)
71
+ return Object.freeze({ ...running, verdict: 'out-of-range' });
72
+ // Nothing is violated but there is no line to state: the page withholds the
73
+ // range rather than inventing one.
74
+ if (range === undefined) {
75
+ return Object.freeze({ ...base, verdict: 'undetermined', reason: 'the declared dsh support range is unreadable or drifted' });
76
+ }
77
+ return Object.freeze({ ...running, verdict: 'ok' });
78
+ }
79
+ /**
80
+ * Read this Host's own environment: the installed bundle version, the declared
81
+ * support line, and how the running DSH compares to it.
82
+ *
83
+ * The certified DSH version is the range's lower bound, never the newest cut
84
+ * tested against it: `docs/dsh-release-compatibility.md` §4 records a baseline
85
+ * inside the declared range without moving the peers, so every version spot
86
+ * keeps naming the bound. Reading it from anywhere else would reintroduce the
87
+ * hand-maintained constant this check exists to remove.
88
+ *
89
+ * @returns the report, with `verdict: 'undetermined'` for any fact not established.
90
+ */
91
+ export function reportEnvironment() {
92
+ const manifest = readInstalledManifest() ?? {};
93
+ const bundleVersion = bundleVersionOf(manifest);
94
+ let runtime;
95
+ try {
96
+ runtime = getDshRuntimeVersion();
97
+ }
98
+ catch (error) {
99
+ return evaluateEnvironment(manifest, 'unknown', bundleVersion, error instanceof Error ? error.message : String(error));
100
+ }
101
+ return evaluateEnvironment(manifest, runtime, bundleVersion);
102
+ }
@@ -1,8 +1,6 @@
1
- import { readFileSync } from 'node:fs';
2
- import { dirname, resolve } from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
1
  import z from '@deepseek-ai/schemastery';
5
2
  import { parse } from 'yaml';
3
+ import { bundleVersionOf, readInstalledManifest } from "./installed-manifest.js";
6
4
  /**
7
5
  * Human identity profile: the one configurable display name plus the avatar
8
6
  * reference. `member:human` stays the durable identity everywhere; only the
@@ -17,6 +15,12 @@ import { parse } from 'yaml';
17
15
  * - avatar bytes live under a persistent directory below; the profile holds
18
16
  * only the reference, never a data URL. The composer attachment cache is
19
17
  * TTL-bound and must not hold avatar bytes.
18
+ *
19
+ * The legacy-section adoption below is transitional: it exists for installs
20
+ * upgraded from a pre-rc.1 profile document, and retires — with
21
+ * `LEGACY_HUMAN_PROFILE_SECTION`, `parseLegacyHumanProfile`, and
22
+ * `planLegacyAdoption` — once such installs are no longer supported. Nothing
23
+ * else in the Host reads that document.
20
24
  */
21
25
  /**
22
26
  * Settings namespace of the Human profile: the Team Host row's id in
@@ -33,36 +37,18 @@ export const HUMAN_PROFILE_DEFAULT_NAME = 'human';
33
37
  export const HUMAN_PROFILE_REPO_URL = 'https://github.com/wowyuarm/dsh-agent-team';
34
38
  /**
35
39
  * Bundle version shown in the settings footnote, and the current side of the
36
- * update check: the version of the package THIS Host runs from — read from the
37
- * installed manifest, so a `link:` checkout under the development profile and a
38
- * registry tarball under stable each state their own truth. It is not a
39
- * hand-maintained string: the 0.1.14 bundle shipped with `0.1.13` written in
40
- * it, which made the footnote name the previous release and the update check
41
- * offer the release the user already had.
40
+ * update check: the version of the package THIS Host runs from — the installed
41
+ * manifest's, read once at load through `./installed-manifest.ts`, so a `link:`
42
+ * checkout under the development profile and a registry tarball under stable
43
+ * each state their own truth. It is not a hand-maintained string: the 0.1.14
44
+ * bundle shipped with `0.1.13` written in it, which made the footnote name the
45
+ * previous release and the update check offer the release the user already had.
42
46
  *
43
- * Resolved once at load, three levels above this module — `packages/agent-team/{src,lib}`
44
- * sits that deep in both layouts, the same relative positioning
45
- * `member-runtime.ts` uses to find `core-skills`. An unreadable or malformed
46
- * manifest degrades to `'unknown'`: the Remote's `version: string` contract
47
- * holds and the update comparison simply compares nothing. The footnote is
48
- * informational only and never gates behavior.
47
+ * An unreadable or malformed manifest degrades to `'unknown'`: the Remote's
48
+ * `version: string` contract holds and the update comparison simply compares
49
+ * nothing. The footnote is informational only and never gates behavior.
49
50
  */
50
- export const HUMAN_PROFILE_VERSION = readInstalledBundleVersion();
51
- /** Version of the manifest this package installed from, or `'unknown'`. */
52
- function readInstalledBundleVersion() {
53
- try {
54
- const manifestPath = resolve(dirname(fileURLToPath(import.meta.url)), '../../../package.json');
55
- const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
56
- if (typeof manifest.version === 'string' && manifest.version !== '')
57
- return manifest.version;
58
- }
59
- catch {
60
- // Reading our own manifest must never fail the Host boot: a host that
61
- // cannot find its own package.json is a broken install, and the footnote
62
- // reporting 'unknown' says so more honestly than a stale number.
63
- }
64
- return 'unknown';
65
- }
51
+ export const HUMAN_PROFILE_VERSION = bundleVersionOf(readInstalledManifest());
66
52
  /**
67
53
  * Schemastery schema of the Human profile: the Team Host row's Config. Both
68
54
  * fields are volatile, which is what lets an edit reach the running Host
@@ -139,12 +125,12 @@ export function parseLegacyHumanProfile(yamlText) {
139
125
  * Decide what adoption may write: nothing unless the stored profile is still
140
126
  * the pristine default, so a value the Human re-entered after the upgrade
141
127
  * always wins, and never the legacy default name itself. The current profile
142
- * carries explicit `undefined` on `avatarRef` — the Host getter spreads a
143
- * settings read — and so is not `HumanProfile` under
144
- * exactOptionalPropertyTypes; taking that shape directly keeps the Host call
145
- * cast-free. The returned fields are exactly the ops the profile page would
146
- * have written; byte existence for `avatarRef` is the caller's I/O and must
147
- * already hold.
128
+ * carries explicit `undefined` on `avatarRef` — the Host getter spreads the
129
+ * live Config read — and that shape does not satisfy the optional-property
130
+ * form under exactOptionalPropertyTypes; taking it directly keeps the Host
131
+ * call cast-free. The returned fields are exactly the ops the profile page
132
+ * would have written; byte existence for `avatarRef` is the caller's I/O and
133
+ * must already hold.
148
134
  */
149
135
  export function planLegacyAdoption(current, legacy) {
150
136
  if (current.name !== HUMAN_PROFILE_DEFAULT_NAME || current.avatarRef !== undefined)