@wowyuarm/dsh-agent-team 0.2.0 → 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.
- package/README.md +5 -5
- package/README.zh.md +5 -5
- package/cordis.patch.yml +26 -12
- package/package.json +50 -47
- package/packages/agent-team/lib/attachments.js +63 -4
- package/packages/agent-team/lib/compaction-engine.js +18 -0
- package/packages/agent-team/lib/context-continuity-host.js +198 -4
- package/packages/agent-team/lib/context-judge.js +150 -0
- package/packages/agent-team/lib/index.js +482 -97
- package/packages/agent-team/lib/invariant.js +36 -12
- package/packages/agent-team/lib/ledger.js +2 -1
- package/packages/agent-team/lib/member-runtime.js +2 -1
- package/packages/agent-team/lib/recovery.js +53 -4
- package/packages/agent-team/lib/typert.host.js +138 -44
- package/packages/agent-team/lib/typert.remote-client.d.ts +3 -1
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +75 -35
- package/packages/agent-team/lib/types/attachments.d.ts +32 -2
- package/packages/agent-team/lib/types/attachments.d.ts.map +1 -1
- package/packages/agent-team/lib/types/compaction-engine.d.ts +19 -0
- package/packages/agent-team/lib/types/compaction-engine.d.ts.map +1 -0
- package/packages/agent-team/lib/types/context-continuity-host.d.ts +131 -4
- package/packages/agent-team/lib/types/context-continuity-host.d.ts.map +1 -1
- package/packages/agent-team/lib/types/context-judge.d.ts +82 -0
- package/packages/agent-team/lib/types/context-judge.d.ts.map +1 -0
- package/packages/agent-team/lib/types/index.d.ts +259 -17
- package/packages/agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/types/invariant.d.ts +27 -5
- package/packages/agent-team/lib/types/invariant.d.ts.map +1 -1
- package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-runtime.d.ts +1 -1
- package/packages/agent-team/lib/types/member-runtime.d.ts.map +1 -1
- package/packages/agent-team/lib/types/recovery.d.ts +19 -6
- package/packages/agent-team/lib/types/recovery.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/requests-results.d.ts +28 -2
- package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types.d.ts +9 -0
- package/packages/agent-team/lib/types/types.d.ts.map +1 -1
- package/packages/client-agent-team/lib/client.js +1006 -374
- package/packages/client-agent-team/lib/client.js.map +1 -1
- package/packages/client-agent-team/lib/types/client/{HumanSettingsSection.d.ts → TeamSettingsSection.d.ts} +12 -4
- package/packages/client-agent-team/lib/types/client/TeamSettingsSection.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/TeamSettingsSection.js +195 -0
- package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts +7 -11
- package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/attachment-preview.js +13 -3
- package/packages/client-agent-team/lib/types/client/context-judge.d.ts +64 -0
- package/packages/client-agent-team/lib/types/client/context-judge.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/context-judge.js +78 -0
- package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/index.js +75 -11
- package/packages/client-agent-team/lib/types/client/judge-form.d.ts +99 -0
- package/packages/client-agent-team/lib/types/client/judge-form.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/judge-form.js +208 -0
- package/packages/client-agent-team/lib/types/client/locales.d.ts +52 -6
- package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/locales.js +55 -6
- package/packages/tool-agent-team/README.md +6 -3
- package/packages/tool-agent-team/README.zh.md +6 -3
- package/packages/tool-agent-team/lib/context-tools.js +84 -112
- package/packages/tool-agent-team/lib/types/context-tools.d.ts +29 -13
- package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -1
- package/packages/agent-team/lib/pressure-policy.js +0 -235
- package/packages/agent-team/lib/types/pressure-policy.d.ts +0 -134
- package/packages/agent-team/lib/types/pressure-policy.d.ts.map +0 -1
- package/packages/client-agent-team/lib/types/client/HumanSettingsSection.d.ts.map +0 -1
- package/packages/client-agent-team/lib/types/client/HumanSettingsSection.js +0 -118
|
@@ -50,15 +50,17 @@ import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
|
50
50
|
import { scopeOf } from '@deepseek-ai/dsh-scope';
|
|
51
51
|
import { Session, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session';
|
|
52
52
|
import { setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy';
|
|
53
|
-
import
|
|
54
|
-
import {
|
|
53
|
+
import z from '@deepseek-ai/schemastery';
|
|
54
|
+
import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
55
|
+
import { ATTACHMENT_MAX_BYTES, attachmentPayloadPath, attachmentsRoot, copyPathAttachment, pathAttachmentId, readAttachment, removeAttachment, sanitizeMediaType, sweepAttachmentCache, validatePathAttachment, writeRequestScopedAttachment } from "./attachments.js";
|
|
55
56
|
import { reportEnvironment } from "./environment-check.js";
|
|
56
57
|
import { HUMAN_PROFILE_DEFAULT_NAME, HUMAN_PROFILE_REPO_URL, HUMAN_PROFILE_SETTINGS_NAMESPACE, HUMAN_PROFILE_SETTINGS_SCHEMA, HUMAN_PROFILE_VERSION, assertValidHumanName, normalizeHumanName, parseLegacyHumanProfile, planLegacyAdoption } from "./human-profile.js";
|
|
57
58
|
import { humanAvatarsRoot, readHumanAvatar, removeHumanAvatar, writeHumanAvatar } from "./human-avatar.js";
|
|
58
59
|
import { createHumanUpdateChecker } from "./human-update-check.js";
|
|
59
|
-
import {
|
|
60
|
-
import {
|
|
61
|
-
import {
|
|
60
|
+
import { AgentTeamInvariantError } from "./invariant.js";
|
|
61
|
+
import { createTeamContextManagement, TeamPressurePolicy, TEAM_CONTEXT_CODEC, TEAM_PRESSURE_GATE_SCHEMA } from "./context-continuity-host.js";
|
|
62
|
+
import { CONTEXT_CONTINUITY_PROJECTION_KEY, DEFAULT_GATE_IDLE_MS, DEFAULT_GATE_TOKENS, readContextTimeline } from '@wowyuarm/dsh-context-continuity';
|
|
63
|
+
import { TeamContextJudge } from "./context-judge.js";
|
|
62
64
|
import { AGENT_TEAM_PLUGIN_ID, isAgentTeamSource } from "./context-source.js";
|
|
63
65
|
import { boundaryByRef, carriedInputOf, checkpointByRef, checkpointRefFor, createTeamContextProjectionConfig, createTeamContextProjectionDefinition, foldTeamContextProjection, retainedTopicsThrough, TeamContextProjectionHost } from "./context-projection.js";
|
|
64
66
|
import { AGENT_TEAM_HUMAN_MEMBER_ID, AgentTeamLedger, agentTeamHumanActor } from "./ledger.js";
|
|
@@ -106,6 +108,19 @@ const CONTEXT_HARD_LIMIT_CAP = 256_000;
|
|
|
106
108
|
const CONTEXT_HANDOFF_AT_CAP = 200_000;
|
|
107
109
|
const CONTEXT_HANDOFF_RESERVE = 8_000;
|
|
108
110
|
const CONTEXT_SAFE_OUTPUT_RESERVE = 16_000;
|
|
111
|
+
/**
|
|
112
|
+
* The budget Team gives the long-gap gate's judge when the row states none.
|
|
113
|
+
*
|
|
114
|
+
* The engine's own default is five seconds, which is sized for a judge beside
|
|
115
|
+
* the Host. A hosted endpoint answers in up to about four seconds, and the gate
|
|
116
|
+
* hands the judge one second less than this deadline, so that default turns a
|
|
117
|
+
* slow-but-working judge into a coin flip — and the deployment that meets this
|
|
118
|
+
* problem is the one least able to diagnose it. Ten seconds leaves room for the
|
|
119
|
+
* slow tail while still bounding the wait on the path that starts a turn. A
|
|
120
|
+
* deployment that wants another number still sets `gate.judgeTimeoutMs`, which
|
|
121
|
+
* stays a row field.
|
|
122
|
+
*/
|
|
123
|
+
const TEAM_JUDGE_TIMEOUT_DEFAULT_MS = 10_000;
|
|
109
124
|
/**
|
|
110
125
|
* Usage at or above which an acknowledged acceptance advises a fresh
|
|
111
126
|
* rollover instead of keeping the context; capped by the route's effective
|
|
@@ -173,6 +188,29 @@ export class AgentTeamDmDeliveryError extends Error {
|
|
|
173
188
|
this.name = 'AgentTeamDmDeliveryError';
|
|
174
189
|
}
|
|
175
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* The judge's fields as one row form. Every one is volatile, because that is
|
|
193
|
+
* what makes the settings service offer them and what lets a key written in the
|
|
194
|
+
* Team settings surface mount the judge without restarting the Host; the key
|
|
195
|
+
* alone is a secret, so a read reports that it is set and never its value.
|
|
196
|
+
*/
|
|
197
|
+
const TEAM_JUDGE_SETTINGS_SCHEMA = z.object({
|
|
198
|
+
apiKey: z.string().role('secret').volatile().description('API key for the relatedness judge. Setting one enables jev-backed context management; clearing it disables it.'),
|
|
199
|
+
apiKeyEnv: z.string().volatile().description('Environment variable holding the key, when the key is not stated here. Default: TYPESAFE_API_KEY'),
|
|
200
|
+
model: z.string().volatile().description('Model id sent with every judgement. Default: the jev plugin\'s own default.'),
|
|
201
|
+
apiBase: z.string().volatile().description('Endpoint base the judgements go to. Default: the jev plugin\'s own default.'),
|
|
202
|
+
});
|
|
203
|
+
/**
|
|
204
|
+
* The Host row's whole schema: the profile fields verbatim, so their volatility
|
|
205
|
+
* and defaults stay owned by {@link HUMAN_PROFILE_SETTINGS_SCHEMA}, plus the
|
|
206
|
+
* gate and the judge. A patch layer that targets this row replaces its whole
|
|
207
|
+
* config, so the parts are declared together here and nowhere else.
|
|
208
|
+
*/
|
|
209
|
+
export const TEAM_HOST_ROW_SETTINGS_SCHEMA = z.object({
|
|
210
|
+
...HUMAN_PROFILE_SETTINGS_SCHEMA.dict,
|
|
211
|
+
gate: TEAM_PRESSURE_GATE_SCHEMA,
|
|
212
|
+
jev: TEAM_JUDGE_SETTINGS_SCHEMA,
|
|
213
|
+
});
|
|
176
214
|
let AgentTeam = (() => {
|
|
177
215
|
let _classSuper = TypertRemoteService;
|
|
178
216
|
let _instanceExtraInitializers = [];
|
|
@@ -197,6 +235,7 @@ let AgentTeam = (() => {
|
|
|
197
235
|
let _sendMessage_decorators;
|
|
198
236
|
let _putAttachment_decorators;
|
|
199
237
|
let _getAttachment_decorators;
|
|
238
|
+
let _contextJudgeForClient_decorators;
|
|
200
239
|
let _environmentForClient_decorators;
|
|
201
240
|
let _humanProfileForClient_decorators;
|
|
202
241
|
let _putHumanAvatar_decorators;
|
|
@@ -234,6 +273,7 @@ let AgentTeam = (() => {
|
|
|
234
273
|
_sendMessage_decorators = [Remote('sendMessage')];
|
|
235
274
|
_putAttachment_decorators = [Remote('putAttachment')];
|
|
236
275
|
_getAttachment_decorators = [Remote('getAttachment')];
|
|
276
|
+
_contextJudgeForClient_decorators = [Remote('contextJudge')];
|
|
237
277
|
_environmentForClient_decorators = [Remote('environment')];
|
|
238
278
|
_humanProfileForClient_decorators = [Remote('humanProfile')];
|
|
239
279
|
_putHumanAvatar_decorators = [Remote('putHumanAvatar')];
|
|
@@ -268,6 +308,7 @@ let AgentTeam = (() => {
|
|
|
268
308
|
__esDecorate(this, null, _sendMessage_decorators, { kind: "method", name: "sendMessage", static: false, private: false, access: { has: obj => "sendMessage" in obj, get: obj => obj.sendMessage }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
269
309
|
__esDecorate(this, null, _putAttachment_decorators, { kind: "method", name: "putAttachment", static: false, private: false, access: { has: obj => "putAttachment" in obj, get: obj => obj.putAttachment }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
270
310
|
__esDecorate(this, null, _getAttachment_decorators, { kind: "method", name: "getAttachment", static: false, private: false, access: { has: obj => "getAttachment" in obj, get: obj => obj.getAttachment }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
311
|
+
__esDecorate(this, null, _contextJudgeForClient_decorators, { kind: "method", name: "contextJudgeForClient", static: false, private: false, access: { has: obj => "contextJudgeForClient" in obj, get: obj => obj.contextJudgeForClient }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
271
312
|
__esDecorate(this, null, _environmentForClient_decorators, { kind: "method", name: "environmentForClient", static: false, private: false, access: { has: obj => "environmentForClient" in obj, get: obj => obj.environmentForClient }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
272
313
|
__esDecorate(this, null, _humanProfileForClient_decorators, { kind: "method", name: "humanProfileForClient", static: false, private: false, access: { has: obj => "humanProfileForClient" in obj, get: obj => obj.humanProfileForClient }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
273
314
|
__esDecorate(this, null, _putHumanAvatar_decorators, { kind: "method", name: "putHumanAvatar", static: false, private: false, access: { has: obj => "putHumanAvatar" in obj, get: obj => obj.putHumanAvatar }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
@@ -284,7 +325,7 @@ let AgentTeam = (() => {
|
|
|
284
325
|
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
285
326
|
}
|
|
286
327
|
config = __runInitializers(this, _instanceExtraInitializers);
|
|
287
|
-
static Config =
|
|
328
|
+
static Config = TEAM_HOST_ROW_SETTINGS_SCHEMA;
|
|
288
329
|
static inject = [
|
|
289
330
|
'storageDomain',
|
|
290
331
|
'workspaceRegistry',
|
|
@@ -333,6 +374,16 @@ let AgentTeam = (() => {
|
|
|
333
374
|
*/
|
|
334
375
|
memberFailures = new Map();
|
|
335
376
|
pressurePolicy;
|
|
377
|
+
/**
|
|
378
|
+
* The gate thresholds the pressure policy reads, and the only copy of them:
|
|
379
|
+
* refreshed in place from the row's volatile config, never replaced, so the
|
|
380
|
+
* engine's per-step read sees a settings edit without a remount. It starts at
|
|
381
|
+
* Team's own judge budget and the constructor's refresh replaces that with the
|
|
382
|
+
* row's, so the budget is never absent at any point a step could read it.
|
|
383
|
+
*/
|
|
384
|
+
gate = { judgeTimeoutMs: TEAM_JUDGE_TIMEOUT_DEFAULT_MS };
|
|
385
|
+
/** The long-gap gate's judge: this Host's own `jev` mount, reconciled with the row. */
|
|
386
|
+
contextJudge;
|
|
336
387
|
notifiedInbox = new Map();
|
|
337
388
|
attachmentGcTimer;
|
|
338
389
|
/**
|
|
@@ -398,21 +449,38 @@ let AgentTeam = (() => {
|
|
|
398
449
|
constructor(ctx, config) {
|
|
399
450
|
super(ctx, 'agentTeam');
|
|
400
451
|
this.config = config;
|
|
401
|
-
this.
|
|
452
|
+
this.refreshGate();
|
|
453
|
+
this.contextJudge = new TeamContextJudge(this.ctx, {
|
|
454
|
+
config: () => this.judgeConfig(),
|
|
455
|
+
deadlineMs: () => this.gate.judgeTimeoutMs ?? TEAM_JUDGE_TIMEOUT_DEFAULT_MS,
|
|
456
|
+
});
|
|
457
|
+
this.pressurePolicy = new TeamPressurePolicy({
|
|
402
458
|
agentForMember: memberId => this.handles.get(memberId)?.agent,
|
|
403
|
-
memberForAgent: agent => {
|
|
404
|
-
const member = this.memberForAgent(agent);
|
|
405
|
-
return member === undefined ? undefined : { memberId: member.memberId, sessionId: agent.id };
|
|
406
|
-
},
|
|
407
|
-
compactionForAgent: agent => this.ctx.agentPresets.serviceFor(agent, 'compaction'),
|
|
408
459
|
limitsForAgent: agent => this.routeLimitsForAgent(agent),
|
|
460
|
+
compactionForAgent: agent => {
|
|
461
|
+
const engine = this.compactionForAgent(agent);
|
|
462
|
+
return engine === undefined ? undefined : { reduce: (reason, signal) => engine.compactIfNeeded(agent, reason, signal) };
|
|
463
|
+
},
|
|
409
464
|
activeClaimLabels: memberId => this.activeClaimLabels(memberId),
|
|
410
465
|
runningJobLabels: memberId => this.runningJobLabels(memberId),
|
|
411
|
-
|
|
466
|
+
// Deployment policy: the row's own gate config, whose omitted fields the
|
|
467
|
+
// engine answers with its own defaults. This object is this Host's one
|
|
468
|
+
// copy, refreshed in place on every settings write, so an edit reaches the
|
|
469
|
+
// next step judged instead of the next mount.
|
|
470
|
+
gate: this.gate,
|
|
471
|
+
/**
|
|
472
|
+
* The relatedness judge of the long-gap gate: the service this Host
|
|
473
|
+
* mounts from its own row's key, or one a deployment mounted itself. No
|
|
474
|
+
* key and no other mount leaves the gate off — a missing judge is a
|
|
475
|
+
* deployment choice, never a failure — and the read is lazy, so a key
|
|
476
|
+
* written after this Host mounted still answers on the next step.
|
|
477
|
+
*/
|
|
478
|
+
judgeForAgent: () => this.contextJudge.judge(),
|
|
479
|
+
failed: (memberId, diagnostic) => {
|
|
412
480
|
this.setMemberFailure(memberId, 'compaction', diagnostic);
|
|
413
481
|
this.emitAutoCompactionChanged(memberId);
|
|
414
482
|
},
|
|
415
|
-
log: message => { this.ctx.logger.warn(`agent-team: ${message}`); },
|
|
483
|
+
log: (message, memberId) => { this.ctx.logger.warn(`agent-team: ${message} (member ${this.memberLabel(memberId)})`); },
|
|
416
484
|
});
|
|
417
485
|
// Human profile: this Host row's own Config (name + avatarRef, both
|
|
418
486
|
// volatile), so the settings service derives its form from the schema and
|
|
@@ -425,7 +493,41 @@ let AgentTeam = (() => {
|
|
|
425
493
|
settingsCtx.effect(() => settingsCtx.settings.configure({ auto: false }, this.ctx.fiber));
|
|
426
494
|
this.adoptLegacyHumanProfile();
|
|
427
495
|
});
|
|
428
|
-
this.ctx.on('loader/volatile-update', () => {
|
|
496
|
+
this.ctx.on('loader/volatile-update', () => {
|
|
497
|
+
this.syncHumanHandle();
|
|
498
|
+
// The whole row is volatile, so every write the settings surface makes is
|
|
499
|
+
// one reconciliation. The gate comes first: the judge's own budget is
|
|
500
|
+
// derived from the gate's deadline, so a write that raises both must not
|
|
501
|
+
// be reconciled against the deadline it replaced.
|
|
502
|
+
this.refreshGate();
|
|
503
|
+
void this.contextJudge.sync().catch(error => {
|
|
504
|
+
this.ctx.logger.warn('agent-team: the relatedness judge could not be reconciled: %s', error instanceof Error ? error.message : String(error));
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
/** The Host row's judge settings, as one plain config for the mount. */
|
|
509
|
+
judgeConfig() {
|
|
510
|
+
return {
|
|
511
|
+
apiKey: this.config.jev.apiKey.get(),
|
|
512
|
+
apiKeyEnv: this.config.jev.apiKeyEnv.get(),
|
|
513
|
+
model: this.config.jev.model.get(),
|
|
514
|
+
apiBase: this.config.jev.apiBase.get(),
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Copy the row's gate thresholds into the one gate object the pressure policy
|
|
519
|
+
* holds. Called once at construction and again on every volatile update, so
|
|
520
|
+
* the policy always reads the thresholds the settings document states.
|
|
521
|
+
*
|
|
522
|
+
* Two fields pass through as the row states them, and the engine answers an
|
|
523
|
+
* omitted one with its own default. The judge's budget does not: a judge is
|
|
524
|
+
* reached over a network, so Team answers for that number itself rather than
|
|
525
|
+
* making every deployment discover the engine's local-call default.
|
|
526
|
+
*/
|
|
527
|
+
refreshGate() {
|
|
528
|
+
this.gate.tokens = this.config.gate.tokens.get();
|
|
529
|
+
this.gate.idleMs = this.config.gate.idleMs.get();
|
|
530
|
+
this.gate.judgeTimeoutMs = this.config.gate.judgeTimeoutMs.get() ?? TEAM_JUDGE_TIMEOUT_DEFAULT_MS;
|
|
429
531
|
}
|
|
430
532
|
/** Current human display name; the single source for team_view and @ matching. */
|
|
431
533
|
humanHandle() {
|
|
@@ -536,6 +638,10 @@ let AgentTeam = (() => {
|
|
|
536
638
|
}
|
|
537
639
|
/** Open the durable ledger and restore every enabled Member independently. */
|
|
538
640
|
async [Service.init]() {
|
|
641
|
+
// The judge is mounted from the row's own key before the first step can be
|
|
642
|
+
// judged: a deployment that states one gets the gate from its first turn,
|
|
643
|
+
// and one that states none keeps the gate off without a failure.
|
|
644
|
+
await this.contextJudge.sync();
|
|
539
645
|
// The continuity projection registers once per Host: the framework keeps
|
|
540
646
|
// one unit per projection key and drives it for every Session, so Team's
|
|
541
647
|
// fold is session-agnostic and the state carries the identity it folds.
|
|
@@ -548,10 +654,10 @@ let AgentTeam = (() => {
|
|
|
548
654
|
return;
|
|
549
655
|
const message = error instanceof Error ? error.message : String(error);
|
|
550
656
|
this.setMemberFailure(member.memberId, 'runtime', message);
|
|
551
|
-
const kind = classifyRecoverableError(
|
|
657
|
+
const kind = classifyRecoverableError(error);
|
|
552
658
|
if (kind !== undefined)
|
|
553
659
|
this.ctx.logger.warn(`agent-team: member '${member.handle}' hit a recoverable ${kind} error; recording a consecutive error occurrence`);
|
|
554
|
-
this.recovery.onError(member.memberId,
|
|
660
|
+
this.recovery.onError(member.memberId, error);
|
|
555
661
|
this.emitMemberPresenceChanged(member);
|
|
556
662
|
});
|
|
557
663
|
this.ctx.on('agent/status', ({ agent, status }) => {
|
|
@@ -601,7 +707,7 @@ let AgentTeam = (() => {
|
|
|
601
707
|
// A successful assistant response ends any open provider-overflow
|
|
602
708
|
// recovery sequence for this Member.
|
|
603
709
|
if (event.type === 'assistant/message')
|
|
604
|
-
this.pressurePolicy.onAssistantMessage(
|
|
710
|
+
this.pressurePolicy.onAssistantMessage(memberId);
|
|
605
711
|
// Context management reacts only after a successful durable tool/result;
|
|
606
712
|
// the projection (not this listener) decides what that means.
|
|
607
713
|
this.contextManagement.onSessionEvent(memberId, handle.agent, event);
|
|
@@ -641,10 +747,35 @@ let AgentTeam = (() => {
|
|
|
641
747
|
const persistedSessions = new Set((await this.persistedSessionHeaders()).map(snapshot => snapshot.header.id));
|
|
642
748
|
await this.sweepWorkspaceParticipations(ledger);
|
|
643
749
|
for (const member of ledger.listMembers()) {
|
|
644
|
-
if (member.state === 'enabled')
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
750
|
+
if (member.state === 'enabled') {
|
|
751
|
+
// A renewal or rollover that committed but never finished leaves no
|
|
752
|
+
// live handle to reveal it: startup derives the outstanding effects
|
|
753
|
+
// from the ledger and the registry's archive set. A Workspace fault
|
|
754
|
+
// must not keep the Member from running on its recorded binding, so
|
|
755
|
+
// the ordinary activation path still runs when the retire half cannot
|
|
756
|
+
// be finished here.
|
|
757
|
+
try {
|
|
758
|
+
await this.finishMemberTransition(member.memberId, { knownSessions: persistedSessions });
|
|
759
|
+
}
|
|
760
|
+
catch (error) {
|
|
761
|
+
this.ctx.logger.warn(`agent-team: could not finish the recorded Session transition for member '${member.handle}': ${error instanceof Error ? error.message : String(error)}`);
|
|
762
|
+
}
|
|
763
|
+
if (!this.handles.has(member.memberId))
|
|
764
|
+
await this.activateMember(member, undefined, persistedSessions);
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
// Every other durable state converges through the same effect table
|
|
768
|
+
// its operation runs after the commit, so a crash or a failed call
|
|
769
|
+
// between commit and effect is finished here. One Member's fault
|
|
770
|
+
// must not fail the boot: it is logged and retried at the next
|
|
771
|
+
// startup, where the call that failed may have healed.
|
|
772
|
+
try {
|
|
773
|
+
await this.convergeMemberEffects(member);
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
776
|
+
this.ctx.logger.warn(`agent-team: could not converge member '${member.handle}' to its '${member.state}' state: ${error instanceof Error ? error.message : String(error)}`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
648
779
|
}
|
|
649
780
|
}
|
|
650
781
|
/**
|
|
@@ -834,13 +965,13 @@ let AgentTeam = (() => {
|
|
|
834
965
|
return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(stored), workspaceIds: ledger.workspacesOf(stored.memberId) });
|
|
835
966
|
});
|
|
836
967
|
}
|
|
837
|
-
/** Commit suspended intent, then
|
|
968
|
+
/** Commit suspended intent, then run the suspended row of the effect table: the owned AgentHandle is disposed, the Session stays. */
|
|
838
969
|
async suspendMember(request) {
|
|
839
970
|
return this.enqueueLifecycle(async () => {
|
|
840
971
|
const result = await this.requireLedger().suspendMember({ ...request, actor: agentTeamHumanActor() });
|
|
841
972
|
if (result.committed)
|
|
842
973
|
this.emitCommitted(result.value.receipt);
|
|
843
|
-
await this.
|
|
974
|
+
await this.convergeMemberEffects(result.value.member);
|
|
844
975
|
return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(result.value.member) });
|
|
845
976
|
});
|
|
846
977
|
}
|
|
@@ -866,7 +997,9 @@ let AgentTeam = (() => {
|
|
|
866
997
|
* continuation prompt into its live session, rebuild it after an orphaned
|
|
867
998
|
* preset composition, or re-run activation when no live session exists.
|
|
868
999
|
* Runtime-only — no ledger operation, no suspend. Taking over manually also
|
|
869
|
-
* cancels any pending automatic recovery episode.
|
|
1000
|
+
* cancels any pending automatic recovery episode. A non-enabled Member
|
|
1001
|
+
* still holding a live handle is an interrupted suspend/archive/remove,
|
|
1002
|
+
* not a stalled one; recovery finishes that cleanup instead of nudging it.
|
|
870
1003
|
*/
|
|
871
1004
|
async recoverMember(request) {
|
|
872
1005
|
this.requireAccepting();
|
|
@@ -874,9 +1007,36 @@ let AgentTeam = (() => {
|
|
|
874
1007
|
if (member === undefined || !this.requireLedger().participatesIn(request.memberId, request.workspaceId))
|
|
875
1008
|
throw new Error(`unknown Member '${request.memberId}' in workspace '${request.workspaceId}'`);
|
|
876
1009
|
this.recovery.stopTracking(request.memberId);
|
|
1010
|
+
const handle = this.handles.get(request.memberId);
|
|
1011
|
+
// A committed renewal or rollover whose retire or activation never finished
|
|
1012
|
+
// is what leaves a Member with no live generation on its binding, or with
|
|
1013
|
+
// the retired generation still live: this explicit recovery path finishes
|
|
1014
|
+
// that recorded transition rather than restarting a Session the ledger has
|
|
1015
|
+
// already moved off. Every other stranded case keeps its old behavior.
|
|
1016
|
+
const stranded = handle === undefined || handle.agent.id !== member.sessionId;
|
|
1017
|
+
if (stranded && member.state === 'enabled' && this.requireLedger().lastTransitionForMember(request.memberId) !== undefined) {
|
|
1018
|
+
this.ctx.logger.info(`agent-team: finishing the recorded Session transition for member '${member.handle}'`);
|
|
1019
|
+
const settled = await this.enqueueLifecycle(async () => {
|
|
1020
|
+
await this.finishMemberTransition(request.memberId);
|
|
1021
|
+
return this.requireLedger().getMember(request.memberId) ?? member;
|
|
1022
|
+
});
|
|
1023
|
+
return Object.freeze({ status: this.memberStatus(settled) });
|
|
1024
|
+
}
|
|
1025
|
+
// A live handle on a non-enabled Member is the residue of a suspend,
|
|
1026
|
+
// archive, or removal whose disposal failed after its commit: the
|
|
1027
|
+
// durable state already says no live generation, so explicit recovery
|
|
1028
|
+
// re-runs that state's effects rather than feeding the stray generation
|
|
1029
|
+
// a resume prompt or rebuilding it as if it were enabled.
|
|
1030
|
+
if (member.state !== 'enabled' && handle !== undefined) {
|
|
1031
|
+
this.ctx.logger.info(`agent-team: finishing the interrupted ${member.state} cleanup for member '${member.handle}'`);
|
|
1032
|
+
const settled = await this.enqueueLifecycle(async () => {
|
|
1033
|
+
await this.convergeMemberEffects(member);
|
|
1034
|
+
return this.requireLedger().getMember(request.memberId) ?? member;
|
|
1035
|
+
});
|
|
1036
|
+
return Object.freeze({ status: this.memberStatus(settled) });
|
|
1037
|
+
}
|
|
877
1038
|
// An orphaned composition cannot be steered: its tools are gone, so a
|
|
878
1039
|
// continuation prompt reaches an inert Member. Rebuild the Agent in place.
|
|
879
|
-
const handle = this.handles.get(request.memberId);
|
|
880
1040
|
if (handle !== undefined && this.ctx.agentPresets.composedPreset(handle.agent.ctx) === undefined) {
|
|
881
1041
|
this.ctx.logger.info(`agent-team: rebuilding member '${member.handle}' after its preset composition was orphaned by a reload`);
|
|
882
1042
|
await this.reactivateMember(request.memberId);
|
|
@@ -920,27 +1080,50 @@ let AgentTeam = (() => {
|
|
|
920
1080
|
throw new Error(`unknown Member '${request.memberId}' in workspace '${request.workspaceId}'`);
|
|
921
1081
|
if (stored.state !== 'enabled')
|
|
922
1082
|
throw new Error(`Agent Member '${stored.handle}' is ${stored.state}; only enabled Members can start from a new context`);
|
|
923
|
-
const active = this.handles.get(request.memberId);
|
|
924
|
-
if (active === undefined)
|
|
925
|
-
throw new Error(`Agent Member '${stored.handle}' has no active session to clear`);
|
|
926
|
-
if (this.runningAgents.has(active.agent.id))
|
|
927
|
-
throw new Error(`Agent Member '${stored.handle}' is still running; wait for the current turn to end before starting from a new context`);
|
|
928
|
-
const previousSessionId = stored.sessionId;
|
|
929
1083
|
// The fresh id derives from the requestId, so a retried identical
|
|
930
1084
|
// request mints the same id and the ledger dedupes it instead of
|
|
931
1085
|
// colliding; the format matches addMember's `agent-team-<uuid>`.
|
|
932
1086
|
const sessionId = SessionId(`agent-team-${request.requestId}`);
|
|
1087
|
+
// A renewal this request already committed IS the desired binding: its
|
|
1088
|
+
// retire or activation effects may still be outstanding, so finish them
|
|
1089
|
+
// first, then answer with the recorded result either way. A repeat never
|
|
1090
|
+
// commits a second binding, and never reports an effect that did not run
|
|
1091
|
+
// as though it had — the status below carries that truth.
|
|
1092
|
+
if (this.requireLedger().hasCommitted(request.requestId)) {
|
|
1093
|
+
try {
|
|
1094
|
+
await this.finishMemberTransition(request.memberId);
|
|
1095
|
+
}
|
|
1096
|
+
catch (error) {
|
|
1097
|
+
this.ctx.logger.warn(`agent-team: could not finish the recorded context renewal for member '${stored.handle}': ${error instanceof Error ? error.message : String(error)}`);
|
|
1098
|
+
}
|
|
1099
|
+
const recorded = await this.requireLedger().renewMemberSession({ ...request, sessionId, actor: agentTeamHumanActor() });
|
|
1100
|
+
const current = this.requireLedger().getMember(request.memberId) ?? stored;
|
|
1101
|
+
return Object.freeze({ receipt: recorded.value.receipt, status: this.memberStatus(current) });
|
|
1102
|
+
}
|
|
1103
|
+
const active = this.handles.get(request.memberId);
|
|
1104
|
+
if (active === undefined) {
|
|
1105
|
+
// With no live generation to retire, an earlier transition may be what
|
|
1106
|
+
// left the Member here; finishing it is what lets this request run at
|
|
1107
|
+
// all. With none, the ordinary no-session refusal below stands.
|
|
1108
|
+
await this.finishMemberTransition(request.memberId);
|
|
1109
|
+
}
|
|
1110
|
+
const retiring = this.handles.get(request.memberId);
|
|
1111
|
+
if (retiring === undefined)
|
|
1112
|
+
throw new Error(`Agent Member '${stored.handle}' has no active session to clear`);
|
|
1113
|
+
if (this.runningAgents.has(retiring.agent.id))
|
|
1114
|
+
throw new Error(`Agent Member '${stored.handle}' is still running; wait for the current turn to end before starting from a new context`);
|
|
933
1115
|
const result = await this.requireLedger().renewMemberSession({ ...request, sessionId, actor: agentTeamHumanActor() });
|
|
934
|
-
if (result.committed)
|
|
935
|
-
this
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(result.value.member) });
|
|
1116
|
+
if (!result.committed) {
|
|
1117
|
+
// Another caller recorded this requestId between the check above and
|
|
1118
|
+
// the write; report its recorded outcome as the retry path does.
|
|
1119
|
+
const current = this.requireLedger().getMember(request.memberId) ?? stored;
|
|
1120
|
+
return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(current) });
|
|
940
1121
|
}
|
|
1122
|
+
this.emitCommitted(result.value.receipt);
|
|
941
1123
|
const renewed = result.value.member;
|
|
942
|
-
|
|
943
|
-
|
|
1124
|
+
// This request retired the previous generation itself, so the activation
|
|
1125
|
+
// is planned rather than recovered: pass the fork parent it just left.
|
|
1126
|
+
await this.finishMemberTransition(renewed.memberId, { forkedFrom: stored.sessionId });
|
|
944
1127
|
const reactivated = this.handles.get(request.memberId);
|
|
945
1128
|
if (reactivated === undefined) {
|
|
946
1129
|
// Reactivation failed; the activation diagnostic carries the reason and
|
|
@@ -950,6 +1133,79 @@ let AgentTeam = (() => {
|
|
|
950
1133
|
return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(renewed) });
|
|
951
1134
|
});
|
|
952
1135
|
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Finish one Member's committed Session transition, whichever half a failure
|
|
1138
|
+
* interrupted. The ledger already records the binding the Member should run
|
|
1139
|
+
* and the Session it left, so only external effects remain and each is
|
|
1140
|
+
* derived fresh: a generation still live on the old Session is retired, the
|
|
1141
|
+
* retired Session is archived once the registry does not yet report it
|
|
1142
|
+
* archived, and the target activates when no live generation runs it.
|
|
1143
|
+
*
|
|
1144
|
+
* A live handle alone never decides completion — disposal can succeed and
|
|
1145
|
+
* leave the archive half owed — so the registry's own archive set is the
|
|
1146
|
+
* evidence for that step.
|
|
1147
|
+
*
|
|
1148
|
+
* `forkedFrom` marks a PLANNED in-process activation (the caller just
|
|
1149
|
+
* retired that Session in this same request): `activateMember` reads its
|
|
1150
|
+
* absence as a recovered Session that may never have materialized and runs
|
|
1151
|
+
* the handoff-reconstruction path. Startup, retry, and explicit recovery
|
|
1152
|
+
* therefore leave it unset; only the request that committed the transition
|
|
1153
|
+
* supplies it.
|
|
1154
|
+
*
|
|
1155
|
+
* Callers must already hold the lifecycle queue (or run before any other
|
|
1156
|
+
* lifecycle work): this method does not re-enter it.
|
|
1157
|
+
*/
|
|
1158
|
+
async finishMemberTransition(memberId, options) {
|
|
1159
|
+
const member = this.requireLedger().getMember(memberId);
|
|
1160
|
+
if (member === undefined || member.state !== 'enabled')
|
|
1161
|
+
return;
|
|
1162
|
+
const transition = this.requireLedger().lastTransitionForMember(memberId);
|
|
1163
|
+
if (transition === undefined)
|
|
1164
|
+
return;
|
|
1165
|
+
const live = this.handles.get(memberId);
|
|
1166
|
+
if (live !== undefined && live.agent.id !== member.sessionId) {
|
|
1167
|
+
await this.retireMemberGeneration(memberId, live, live.agent.id);
|
|
1168
|
+
}
|
|
1169
|
+
if (!this.ctx.workspaceRegistry.archivedSessionIds.includes(transition.previousSessionId)) {
|
|
1170
|
+
await this.ctx.workspaceRegistry.archiveSession(transition.previousSessionId);
|
|
1171
|
+
}
|
|
1172
|
+
if (this.handles.get(memberId) === undefined) {
|
|
1173
|
+
await this.activateMember(member, undefined, options?.knownSessions, options?.forkedFrom);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Converge one Member's external effects onto its durable lifecycle state
|
|
1178
|
+
* — the single effect table every lifecycle operation runs right after its
|
|
1179
|
+
* commit and startup re-runs for every non-enabled Member:
|
|
1180
|
+
*
|
|
1181
|
+
* - `suspended`: no live handle; the Session and private memory stay.
|
|
1182
|
+
* - `archived`: no live handle; the Session is hidden from grouping
|
|
1183
|
+
* surfaces while its log and private memory stay on disk.
|
|
1184
|
+
* - `inactive`: no live handle; the Session is archived and the private
|
|
1185
|
+
* memory deleted — removeMember's cleanup primitives.
|
|
1186
|
+
*
|
|
1187
|
+
* `enabled` has no entry here: its effects belong to the recorded Session
|
|
1188
|
+
* transition and activation. Every step is derived from the durable state
|
|
1189
|
+
* and safe to repeat — the registry's archive set decides whether the
|
|
1190
|
+
* hide half is owed, disposal tolerates an absent handle, and removal
|
|
1191
|
+
* cleanup force-tolerates an already-cleaned target — so a repeat after a
|
|
1192
|
+
* crash or a failed call runs exactly the owed steps and never a second
|
|
1193
|
+
* durable write. Callers must already hold the lifecycle queue (or run
|
|
1194
|
+
* before any other lifecycle work, as startup does).
|
|
1195
|
+
*/
|
|
1196
|
+
async convergeMemberEffects(member) {
|
|
1197
|
+
if (member.state === 'enabled')
|
|
1198
|
+
return;
|
|
1199
|
+
await this.disposeMemberSession(member.memberId, member);
|
|
1200
|
+
if (member.state === 'archived') {
|
|
1201
|
+
if (!this.ctx.workspaceRegistry.archivedSessionIds.includes(member.sessionId)) {
|
|
1202
|
+
await this.ctx.workspaceRegistry.archiveSession(member.sessionId);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
else if (member.state === 'inactive') {
|
|
1206
|
+
await this.memberRuntime.cleanupRemovedMember(member);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
953
1209
|
/**
|
|
954
1210
|
* Retire one Member's previous generation after its durable binding moved
|
|
955
1211
|
* onto a new Session id: drop the old handle's transient state, dispose the
|
|
@@ -1179,8 +1435,7 @@ let AgentTeam = (() => {
|
|
|
1179
1435
|
const result = await this.requireLedger().removeMember({ ...request, actor: agentTeamHumanActor() });
|
|
1180
1436
|
if (result.committed)
|
|
1181
1437
|
this.emitCommitted(result.value.receipt);
|
|
1182
|
-
await this.
|
|
1183
|
-
await this.memberRuntime.cleanupRemovedMember(result.value.member);
|
|
1438
|
+
await this.convergeMemberEffects(result.value.member);
|
|
1184
1439
|
return result.value;
|
|
1185
1440
|
});
|
|
1186
1441
|
}
|
|
@@ -1195,8 +1450,7 @@ let AgentTeam = (() => {
|
|
|
1195
1450
|
const result = await this.requireLedger().archiveMember({ ...request, actor: agentTeamHumanActor() });
|
|
1196
1451
|
if (result.committed)
|
|
1197
1452
|
this.emitCommitted(result.value.receipt);
|
|
1198
|
-
await this.
|
|
1199
|
-
await this.ctx.workspaceRegistry.archiveSession(result.value.member.sessionId);
|
|
1453
|
+
await this.convergeMemberEffects(result.value.member);
|
|
1200
1454
|
return result.value;
|
|
1201
1455
|
});
|
|
1202
1456
|
}
|
|
@@ -1258,15 +1512,18 @@ let AgentTeam = (() => {
|
|
|
1258
1512
|
/**
|
|
1259
1513
|
* Resolve uploaded ids and agent-supplied absolute paths into one attachment
|
|
1260
1514
|
* metadata list. Paths are all validated before anything is copied, so one
|
|
1261
|
-
* rejection leaves the cache untouched and the message uncommitted.
|
|
1515
|
+
* rejection leaves the cache untouched and the message uncommitted. Each copy
|
|
1516
|
+
* takes the id its request derives, so a retry converges on the entry the
|
|
1517
|
+
* first attempt prepared instead of writing an orphan.
|
|
1262
1518
|
*/
|
|
1263
1519
|
async resolveMessageAttachments(request) {
|
|
1264
1520
|
const fromPaths = [];
|
|
1265
1521
|
if (request.attachmentPaths !== undefined && request.attachmentPaths.length > 0) {
|
|
1266
1522
|
for (const absolutePath of request.attachmentPaths)
|
|
1267
1523
|
await validatePathAttachment(absolutePath);
|
|
1268
|
-
for (const absolutePath of request.attachmentPaths)
|
|
1269
|
-
fromPaths.push(Object.freeze(await copyPathAttachment(attachmentsRoot(), absolutePath)));
|
|
1524
|
+
for (const [index, absolutePath] of request.attachmentPaths.entries()) {
|
|
1525
|
+
fromPaths.push(Object.freeze(await copyPathAttachment(attachmentsRoot(), absolutePath, pathAttachmentId(request.requestId, index), request.requestId)));
|
|
1526
|
+
}
|
|
1270
1527
|
}
|
|
1271
1528
|
return [...fromPaths, ...await this.prepareAttachments(request.attachments)];
|
|
1272
1529
|
}
|
|
@@ -1294,7 +1551,12 @@ let AgentTeam = (() => {
|
|
|
1294
1551
|
const lines = metadata.map(attachment => `[attachment] ${attachmentPayloadPath(attachment.attachmentId, attachment.name)}`);
|
|
1295
1552
|
return `${trimmed}\n${lines.join('\n')}`;
|
|
1296
1553
|
}
|
|
1297
|
-
/**
|
|
1554
|
+
/**
|
|
1555
|
+
* Upload one composer attachment into the cache; bytes are immutable once
|
|
1556
|
+
* written. The requestId is the idempotency key: a retried upload derives
|
|
1557
|
+
* the same cache id and replays the original result, while reusing that key
|
|
1558
|
+
* for a different payload is refused as a request collision.
|
|
1559
|
+
*/
|
|
1298
1560
|
async putAttachment(request) {
|
|
1299
1561
|
this.requireAccepting();
|
|
1300
1562
|
this.requireWorkspace(request.workspaceId);
|
|
@@ -1304,16 +1566,45 @@ let AgentTeam = (() => {
|
|
|
1304
1566
|
if (bytes.byteLength > ATTACHMENT_MAX_BYTES)
|
|
1305
1567
|
throw new Error(`attachment exceeds the ${ATTACHMENT_MAX_BYTES} byte limit`);
|
|
1306
1568
|
const mediaType = sanitizeMediaType(request.mediaType);
|
|
1307
|
-
|
|
1308
|
-
return Object.freeze(await writeAttachment(attachmentsRoot(), attachmentId, request.name, mediaType, bytes));
|
|
1569
|
+
return Object.freeze(await writeRequestScopedAttachment(attachmentsRoot(), request.requestId, request.name, mediaType, bytes));
|
|
1309
1570
|
}
|
|
1310
|
-
/**
|
|
1571
|
+
/**
|
|
1572
|
+
* Read one cached attachment back for client display. A gone entry is the
|
|
1573
|
+
* one expected failure on this path, so it carries the stable
|
|
1574
|
+
* `team/attachment-not-found` code the Client branches on; anything else
|
|
1575
|
+
* (unreadable metadata, filesystem trouble) stays an unknown Host error.
|
|
1576
|
+
*/
|
|
1311
1577
|
async getAttachment(request) {
|
|
1312
1578
|
const stored = await readAttachment(attachmentsRoot(), request.attachmentId);
|
|
1313
1579
|
if (stored === undefined)
|
|
1314
|
-
throw new
|
|
1580
|
+
throw new RemoteError('team/attachment-not-found', `attachment '${request.attachmentId}' is no longer cached`, { attachmentId: request.attachmentId });
|
|
1315
1581
|
return Object.freeze({ name: stored.name, mediaType: stored.mediaType, byteSize: stored.byteSize, bytesBase64: stored.bytes.toString('base64') });
|
|
1316
1582
|
}
|
|
1583
|
+
/**
|
|
1584
|
+
* The long-gap gate's judge, as the Team settings surface states it: whether
|
|
1585
|
+
* a judge is reachable, why not when none is, and the thresholds in force.
|
|
1586
|
+
* The judge's own fields (key presence, endpoint, model) ride the settings
|
|
1587
|
+
* document this row already publishes, so nothing here restates them; the
|
|
1588
|
+
* gate's thresholds are row configuration the settings document deliberately
|
|
1589
|
+
* omits, which is why the Host resolves an omitted field here rather than
|
|
1590
|
+
* letting a second copy of those numbers exist. Two of the three fall back to
|
|
1591
|
+
* the engine's own default; the judge's budget falls back to Team's, because
|
|
1592
|
+
* that is the number the gate itself is given.
|
|
1593
|
+
*/
|
|
1594
|
+
contextJudgeForClient(_request) {
|
|
1595
|
+
const status = this.contextJudge.status();
|
|
1596
|
+
const gate = this.gate;
|
|
1597
|
+
return Object.freeze({
|
|
1598
|
+
...status,
|
|
1599
|
+
gate: Object.freeze({
|
|
1600
|
+
tokens: gate.tokens ?? DEFAULT_GATE_TOKENS,
|
|
1601
|
+
idleMs: gate.idleMs ?? DEFAULT_GATE_IDLE_MS,
|
|
1602
|
+
// No fallback here: this number is the one the gate is given, so a
|
|
1603
|
+
// second default could only ever disagree with the gate in force.
|
|
1604
|
+
judgeTimeoutMs: gate.judgeTimeoutMs,
|
|
1605
|
+
}),
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1317
1608
|
/**
|
|
1318
1609
|
* Human profile read for the settings page and footnote: name + avatar
|
|
1319
1610
|
* reference from this Host row's live Config plus version facts. Human-scoped
|
|
@@ -1541,8 +1832,8 @@ let AgentTeam = (() => {
|
|
|
1541
1832
|
const taskBoundaryThreshold = Math.min(ACCEPT_TASK_BOUNDARY_THRESHOLD, limits.handoffAt);
|
|
1542
1833
|
if (limits.usageTokens >= limits.handoffAt) {
|
|
1543
1834
|
return Object.freeze({ usageTokens: limits.usageTokens, taskBoundaryThreshold, handoffAt: limits.handoffAt, hardLimit: limits.hardLimit,
|
|
1544
|
-
action: '
|
|
1545
|
-
guidance: 'You are at or above the handoff budget. Finish the current atomic action and unsettled evidence, then call
|
|
1835
|
+
action: 'compact-now',
|
|
1836
|
+
guidance: 'You are at or above the handoff budget. Finish the current atomic action and unsettled evidence, then call context_compact with a summary you write yourself — that shortens this generation in place, keeps your recent work verbatim, and is the default. Roll over only when the work has turned a page.' });
|
|
1546
1837
|
}
|
|
1547
1838
|
if (limits.usageTokens >= taskBoundaryThreshold) {
|
|
1548
1839
|
return Object.freeze({ usageTokens: limits.usageTokens, taskBoundaryThreshold, handoffAt: limits.handoffAt, hardLimit: limits.hardLimit,
|
|
@@ -1972,6 +2263,16 @@ let AgentTeam = (() => {
|
|
|
1972
2263
|
this.ctx.logger.info(`agent-team: redelivered ${redelivered} carried input message(s) for member '${this.memberLabel(member.memberId)}' after the rollover crash recovery`);
|
|
1973
2264
|
return redelivered;
|
|
1974
2265
|
}
|
|
2266
|
+
/**
|
|
2267
|
+
* The compaction capability in one Member Agent's preset scope, or undefined
|
|
2268
|
+
* when its composition mounts none. Both the pressure policy (the hard limit)
|
|
2269
|
+
* and the engine's `context_compact` tool ask through here, so which realm
|
|
2270
|
+
* holds the engine is stated once: a preset revision publishes services
|
|
2271
|
+
* behind its own isolate, which `agent.ctx.get` cannot see from outside.
|
|
2272
|
+
*/
|
|
2273
|
+
compactionForAgent(agent) {
|
|
2274
|
+
return this.ctx.agentPresets.serviceFor(agent, 'compaction');
|
|
2275
|
+
}
|
|
1975
2276
|
/**
|
|
1976
2277
|
* Agent-only checkpoint request validation: the tool calls this inside its
|
|
1977
2278
|
* own running turn. Like `context_rollover`, the tool performs no side effect —
|
|
@@ -2007,10 +2308,10 @@ let AgentTeam = (() => {
|
|
|
2007
2308
|
async contextTimelineForAgent(agent, request) {
|
|
2008
2309
|
const member = this.memberForAgent(agent);
|
|
2009
2310
|
if (member === undefined || member.state !== 'enabled')
|
|
2010
|
-
throw new Error('
|
|
2311
|
+
throw new Error('context_status requires an active Team Member');
|
|
2011
2312
|
const limit = request.limit === undefined ? DEFAULT_TIMELINE_LIMIT : Math.trunc(request.limit);
|
|
2012
2313
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_TIMELINE_LIMIT)
|
|
2013
|
-
throw new Error(`
|
|
2314
|
+
throw new Error(`context_status limit must be between 1 and ${MAX_TIMELINE_LIMIT}`);
|
|
2014
2315
|
const meter = agent.ctx.get('tokenMeter');
|
|
2015
2316
|
const measurement = meter?.measure(agent.session);
|
|
2016
2317
|
const usageTokens = measurement?.totalTokens ?? 0;
|
|
@@ -2225,7 +2526,7 @@ let AgentTeam = (() => {
|
|
|
2225
2526
|
if (request.checkpointRef === undefined)
|
|
2226
2527
|
return { mode: 'fresh' };
|
|
2227
2528
|
if (!/^(context-checkpoint-[0-9a-f]{64}|team-boundary-[0-9a-f]{64})$/.test(request.checkpointRef))
|
|
2228
|
-
throw new Error('checkpointRef must be an opaque ref exactly as returned by
|
|
2529
|
+
throw new Error('checkpointRef must be an opaque ref exactly as returned by context_status');
|
|
2229
2530
|
// Full current-state prevalidation through the ONE resolver the swap
|
|
2230
2531
|
// itself uses: a ref that is fabricated, unattributable, nonshrinking,
|
|
2231
2532
|
// unmeasurable, over-budget, or blocked by multiple active Claims
|
|
@@ -2278,25 +2579,69 @@ let AgentTeam = (() => {
|
|
|
2278
2579
|
}
|
|
2279
2580
|
/** Shared Task-creation commit: resolve uploads into metadata lines and append through the ledger. */
|
|
2280
2581
|
async sendMessageAs(actor, request) {
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2582
|
+
try {
|
|
2583
|
+
const metadata = await this.resolveMessageAttachments(request);
|
|
2584
|
+
const result = await this.requireLedger().sendMessage({
|
|
2585
|
+
...request, body: this.appendAttachmentLines(request.body, metadata),
|
|
2586
|
+
...(metadata.length === 0 ? {} : { resolvedAttachments: metadata }),
|
|
2587
|
+
actor,
|
|
2588
|
+
});
|
|
2589
|
+
this.emitCommittedOutcome(result);
|
|
2590
|
+
return result.value;
|
|
2591
|
+
}
|
|
2592
|
+
catch (error) {
|
|
2593
|
+
await this.dropUnreferencedPrepared(request, error);
|
|
2594
|
+
throw error;
|
|
2595
|
+
}
|
|
2289
2596
|
}
|
|
2290
2597
|
/** Shared existing-Thread reply commit: same upload resolution and outcome emission. */
|
|
2291
2598
|
async replyAs(actor, request) {
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2599
|
+
try {
|
|
2600
|
+
const metadata = await this.resolveMessageAttachments(request);
|
|
2601
|
+
const result = await this.requireLedger().reply({
|
|
2602
|
+
...request, body: this.appendAttachmentLines(request.body, metadata),
|
|
2603
|
+
...(metadata.length === 0 ? {} : { resolvedAttachments: metadata }),
|
|
2604
|
+
actor,
|
|
2605
|
+
});
|
|
2606
|
+
this.emitCommittedOutcome(result);
|
|
2607
|
+
return result.value;
|
|
2608
|
+
}
|
|
2609
|
+
catch (error) {
|
|
2610
|
+
await this.dropUnreferencedPrepared(request, error);
|
|
2611
|
+
throw error;
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
/**
|
|
2615
|
+
* After a failed message or reply attempt, remove the cache entries this
|
|
2616
|
+
* attempt could have prepared — the request-stable path copies — unless a
|
|
2617
|
+
* committed Message references them. The ledger's own reference set is the
|
|
2618
|
+
* guard, so a post-commit failure or a colliding retry can never delete bytes
|
|
2619
|
+
* a stored Message still points at; uploads are untouched, because an
|
|
2620
|
+
* unreferenced upload is a legitimate state the GC owns. Cleanup failures are
|
|
2621
|
+
* logged and never mask the original error.
|
|
2622
|
+
*/
|
|
2623
|
+
async dropUnreferencedPrepared(request, cause) {
|
|
2624
|
+
const prepared = (request.attachmentPaths ?? []).map((_, index) => pathAttachmentId(request.requestId, index));
|
|
2625
|
+
if (prepared.length === 0)
|
|
2626
|
+
return;
|
|
2627
|
+
let referenced;
|
|
2628
|
+
try {
|
|
2629
|
+
referenced = this.requireLedger().referencedAttachmentIds();
|
|
2630
|
+
}
|
|
2631
|
+
catch (failure) {
|
|
2632
|
+
this.ctx.logger.warn(`agent-team: could not consult the attachment reference set after a failed message attempt (${String(cause)}): ${String(failure)}`);
|
|
2633
|
+
return;
|
|
2634
|
+
}
|
|
2635
|
+
for (const attachmentId of prepared) {
|
|
2636
|
+
if (referenced.has(attachmentId))
|
|
2637
|
+
continue;
|
|
2638
|
+
try {
|
|
2639
|
+
await removeAttachment(attachmentsRoot(), attachmentId);
|
|
2640
|
+
}
|
|
2641
|
+
catch (failure) {
|
|
2642
|
+
this.ctx.logger.warn(`agent-team: could not drop unreferenced prepared attachment '${attachmentId}' after a failed message attempt: ${String(failure)}`);
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2300
2645
|
}
|
|
2301
2646
|
/** Fence one Human Remote call: accepting Host, known Workspace, Human actor. */
|
|
2302
2647
|
humanCall(workspaceId) {
|
|
@@ -2417,12 +2762,26 @@ let AgentTeam = (() => {
|
|
|
2417
2762
|
this.contextManagement.captureClaimedInput(agent, messages);
|
|
2418
2763
|
return { kind: 'reject' };
|
|
2419
2764
|
}
|
|
2420
|
-
//
|
|
2765
|
+
// The engine's pressure policy rides the same pre-step seam after the
|
|
2421
2766
|
// admission gate: the handoff-budget notice steers into the running
|
|
2422
|
-
// turn,
|
|
2767
|
+
// turn, the hard limit forces a reduction before the request is
|
|
2423
2768
|
// forwarded — failing closed blocks the step instead of submitting
|
|
2424
|
-
// over the
|
|
2425
|
-
|
|
2769
|
+
// over the limit — and a step arriving into a large context after a
|
|
2770
|
+
// long gap may be held instead of admitted.
|
|
2771
|
+
const member = this.memberForAgent(agent);
|
|
2772
|
+
const pressure = member === undefined
|
|
2773
|
+
? { kind: 'continue' }
|
|
2774
|
+
: await this.pressurePolicy.onPreStep(member.memberId, messages, signal);
|
|
2775
|
+
if (pressure.kind === 'hold') {
|
|
2776
|
+
// The gate kept the messages this step had claimed, and its one
|
|
2777
|
+
// rollover instruction is already steered into their place. Arming
|
|
2778
|
+
// the hold and keeping those messages are one call; the coordinator
|
|
2779
|
+
// owns both exits from here — the input comes back once the driver
|
|
2780
|
+
// converges, or rides into the next generation if the rollover
|
|
2781
|
+
// lands — so this turn ends with nothing further to do.
|
|
2782
|
+
this.contextManagement.holdClaimedInput(agent, messages);
|
|
2783
|
+
return { kind: 'reject' };
|
|
2784
|
+
}
|
|
2426
2785
|
if (pressure.kind === 'reject')
|
|
2427
2786
|
return { kind: 'reject' };
|
|
2428
2787
|
const decision = await next();
|
|
@@ -2431,10 +2790,11 @@ let AgentTeam = (() => {
|
|
|
2431
2790
|
this.contextManagement.captureClaimedInput(agent, messages);
|
|
2432
2791
|
return { kind: 'reject' };
|
|
2433
2792
|
});
|
|
2434
|
-
// Provider context-overflow recovery: one bounded
|
|
2435
|
-
// sequence per failure chain through the
|
|
2793
|
+
// Provider context-overflow recovery: one bounded reduce-and-retry
|
|
2794
|
+
// sequence per failure chain through the engine's policy.
|
|
2436
2795
|
agentCtx.on('agent/request-error', async ({ agent, failure, signal }, next) => {
|
|
2437
|
-
const
|
|
2796
|
+
const member = this.memberForAgent(agent);
|
|
2797
|
+
const retry = member !== undefined && await this.pressurePolicy.onRequestError(member.memberId, failure, signal);
|
|
2438
2798
|
if (retry)
|
|
2439
2799
|
return { kind: 'retry' };
|
|
2440
2800
|
return next();
|
|
@@ -2735,42 +3095,67 @@ let AgentTeam = (() => {
|
|
|
2735
3095
|
throw new Error('agent-team service is shutting down');
|
|
2736
3096
|
}
|
|
2737
3097
|
emitCommitted(receipt) {
|
|
2738
|
-
|
|
3098
|
+
// Everything below runs after the ledger append, so nothing here may turn
|
|
3099
|
+
// a committed operation into a failed Remote call. Each effect that runs
|
|
3100
|
+
// third-party code — Cordis listeners, change waiters, agent wakes — goes
|
|
3101
|
+
// through `afterCommit`; the ordering itself is unchanged: listeners,
|
|
3102
|
+
// then Client invalidation, then the participation notice, then wakes.
|
|
3103
|
+
this.afterCommit('commit listener dispatch', () => this.ctx.emit('agent-team/committed', { receipt }));
|
|
2739
3104
|
const operation = this.ledger?.getOperation(receipt.operationId);
|
|
2740
3105
|
if (operation === undefined) {
|
|
2741
|
-
this.emitChanged();
|
|
3106
|
+
this.afterCommit('change invalidation', () => this.emitChanged());
|
|
2742
3107
|
return;
|
|
2743
3108
|
}
|
|
2744
3109
|
const ledger = this.requireLedger();
|
|
2745
|
-
this.emitChanged(ledger.changeScopesOf(operation));
|
|
3110
|
+
this.afterCommit('change invalidation', () => this.emitChanged(ledger.changeScopesOf(operation)));
|
|
2746
3111
|
if (operation.kind === 'team/member-workspace-joined' || operation.kind === 'team/member-workspace-left') {
|
|
2747
|
-
|
|
2748
|
-
|
|
3112
|
+
this.afterCommit('participation notice', () => {
|
|
3113
|
+
const agent = this.handles.get(operation.data.memberId)?.agent;
|
|
3114
|
+
if (agent === undefined)
|
|
3115
|
+
return;
|
|
2749
3116
|
const path = this.ctx.workspaceRegistry.get(operation.data.workspaceId)?.path;
|
|
2750
3117
|
const text = `Team participation changed: you have ${operation.kind === 'team/member-workspace-joined' ? 'joined' : 'left'} Workspace ${operation.data.workspaceId}${path === undefined ? ' (path unavailable)' : ` (${JSON.stringify(path)})`}.\nCurrent Workspace ids: ${ledger.workspacesOf(operation.data.memberId).join(', ')}. This replaces earlier participation information. Your Session and cwd have not moved.`;
|
|
2751
3118
|
const notice = createUserMessage({ content: [{ type: 'text', text }],
|
|
2752
3119
|
source: { kind: AGENT_TEAM_PLUGIN_ID, form: 'notice', summary: 'Team Workspace participation changed' } });
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
// Notification delivery cannot roll back a committed participation.
|
|
2761
|
-
this.ctx.logger.warn(`agent-team: participation notice not delivered: ${error instanceof Error ? error.message : String(error)}`);
|
|
2762
|
-
}
|
|
2763
|
-
}
|
|
3120
|
+
// Delivered from durable facts; a failed wake cannot roll back the
|
|
3121
|
+
// committed participation — the next notice rederives it.
|
|
3122
|
+
if (agent.status === 'idle' || agent.inbox.nextTurn.some(message => message.source.kind === 'user'))
|
|
3123
|
+
agent.followup(notice);
|
|
3124
|
+
else
|
|
3125
|
+
agent.steer(notice);
|
|
3126
|
+
});
|
|
2764
3127
|
}
|
|
2765
3128
|
for (const memberId of ledger.affectedMembersOf(operation)) {
|
|
2766
3129
|
const handle = this.handles.get(memberId);
|
|
2767
3130
|
if (handle !== undefined)
|
|
2768
|
-
this.notifyMember(handle.agent);
|
|
3131
|
+
this.afterCommit(`notification for '${this.memberLabel(memberId)}'`, () => this.notifyMember(handle.agent));
|
|
2769
3132
|
}
|
|
2770
3133
|
// Task acceptance no longer schedules standalone auto compaction: it is
|
|
2771
3134
|
// a semantic checkpoint/context cue (delivered as ordinary Team
|
|
2772
3135
|
// notification), and the Team pressure policy owns compaction entry.
|
|
2773
3136
|
}
|
|
3137
|
+
/**
|
|
3138
|
+
* Run one effect that follows the ledger append. The operation is durable by
|
|
3139
|
+
* the time this runs, so a delivery or listener failure degrades to a log
|
|
3140
|
+
* line: the Remote answer stays the committed result instead of a false
|
|
3141
|
+
* rollback, and an Inbox wake that failed clears its own signature, so the
|
|
3142
|
+
* next commit touching that Member rederives the same durable facts and
|
|
3143
|
+
* retries without appending a second operation.
|
|
3144
|
+
*
|
|
3145
|
+
* An invariant divergence is the deliberate exception — the invariant
|
|
3146
|
+
* companion re-raises it on the caller's frame precisely to stay loud, and
|
|
3147
|
+
* swallowing it would silence a failed integrity check.
|
|
3148
|
+
*/
|
|
3149
|
+
afterCommit(label, effect) {
|
|
3150
|
+
try {
|
|
3151
|
+
effect();
|
|
3152
|
+
}
|
|
3153
|
+
catch (error) {
|
|
3154
|
+
if (error instanceof AgentTeamInvariantError)
|
|
3155
|
+
throw error;
|
|
3156
|
+
this.ctx.logger.warn(`agent-team: post-commit ${label} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
2774
3159
|
/** Model-visible active-Claim labels for the pressure notice. */
|
|
2775
3160
|
activeClaimLabels(memberId) {
|
|
2776
3161
|
const ledger = this.ledger;
|