@private.me/xcontinuity 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +123 -0
- package/LICENSE.md +26 -0
- package/MIGRATING.md +77 -0
- package/README.md +601 -0
- package/dist/adjudicator.d.ts +75 -0
- package/dist/adjudicator.js +184 -0
- package/dist/cascade.d.ts +157 -0
- package/dist/cascade.js +323 -0
- package/dist/chronicle.d.ts +76 -0
- package/dist/chronicle.js +173 -0
- package/dist/cjs/adjudicator.js +189 -0
- package/dist/cjs/cascade.js +328 -0
- package/dist/cjs/chronicle.js +178 -0
- package/dist/cjs/enforcement.js +108 -0
- package/dist/cjs/errors.js +72 -0
- package/dist/cjs/index.js +108 -0
- package/dist/cjs/memory-runtime.js +129 -0
- package/dist/cjs/memory-session.js +134 -0
- package/dist/cjs/mission.js +178 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/provenance.js +192 -0
- package/dist/cjs/ratification.js +322 -0
- package/dist/cjs/reverse-xorida.js +506 -0
- package/dist/cjs/session.js +273 -0
- package/dist/cjs/state-serializer.js +300 -0
- package/dist/cjs/store-memory.js +33 -0
- package/dist/cjs/trust.js +133 -0
- package/dist/cjs/types.js +59 -0
- package/dist/enforcement.d.ts +40 -0
- package/dist/enforcement.js +104 -0
- package/dist/errors.d.ts +25 -0
- package/dist/errors.js +68 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +43 -0
- package/dist/memory-runtime.d.ts +36 -0
- package/dist/memory-runtime.js +125 -0
- package/dist/memory-session.d.ts +38 -0
- package/dist/memory-session.js +97 -0
- package/dist/mission.d.ts +68 -0
- package/dist/mission.js +172 -0
- package/dist/provenance.d.ts +54 -0
- package/dist/provenance.js +182 -0
- package/dist/ratification.d.ts +113 -0
- package/dist/ratification.js +317 -0
- package/dist/reverse-xorida.d.ts +174 -0
- package/dist/reverse-xorida.js +490 -0
- package/dist/session.d.ts +102 -0
- package/dist/session.js +269 -0
- package/dist/state-serializer.d.ts +37 -0
- package/dist/state-serializer.js +294 -0
- package/dist/store-memory.d.ts +18 -0
- package/dist/store-memory.js +29 -0
- package/dist/trust.d.ts +76 -0
- package/dist/trust.js +121 -0
- package/dist/types.d.ts +320 -0
- package/dist/types.js +56 -0
- package/llms.txt +43 -0
- package/package.json +125 -0
- package/share1.dat +0 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @private.me/xcontinuity — Adjudicator (Conflict Resolution)
|
|
3
|
+
*
|
|
4
|
+
* Resolves conflicts when multiple entries exist for the same key.
|
|
5
|
+
*
|
|
6
|
+
* Two implementations:
|
|
7
|
+
* PolicyAdjudicator — deterministic local resolution (default)
|
|
8
|
+
* ConsensusAdjudicator — multi-agent IXorIDA-backed resolution
|
|
9
|
+
*
|
|
10
|
+
* Deterministic tiebreaker (total ordering):
|
|
11
|
+
* 1. Highest trust tier
|
|
12
|
+
* 2. Newest timestamp
|
|
13
|
+
* 3. Lexicographically lowest author DID
|
|
14
|
+
*
|
|
15
|
+
* This guarantees two agents merging the same conflicting entries
|
|
16
|
+
* always pick the same winner — convergent merge without human input.
|
|
17
|
+
*/
|
|
18
|
+
import { ok, err } from '@private.me/shared';
|
|
19
|
+
import { TRUST_TIER_RANK } from './types.js';
|
|
20
|
+
import { continuityError } from './errors.js';
|
|
21
|
+
import { effectiveTier } from './trust.js';
|
|
22
|
+
/**
|
|
23
|
+
* Policy-based adjudicator (default).
|
|
24
|
+
*
|
|
25
|
+
* Deterministic resolution using total ordering:
|
|
26
|
+
* 1. Highest effective trust tier (with decay applied)
|
|
27
|
+
* 2. Newest timestamp (higher = more recent)
|
|
28
|
+
* 3. Lexicographically lowest author DID (deterministic tiebreaker)
|
|
29
|
+
*/
|
|
30
|
+
export class PolicyAdjudicator {
|
|
31
|
+
resolve(key, candidates) {
|
|
32
|
+
if (candidates.length === 0) {
|
|
33
|
+
return err(continuityError('NO_CANDIDATES', `No candidates for key "${key}"`));
|
|
34
|
+
}
|
|
35
|
+
if (candidates.length === 1) {
|
|
36
|
+
return ok({ winner: candidates[0], reason: 'single candidate' });
|
|
37
|
+
}
|
|
38
|
+
let winner = candidates[0];
|
|
39
|
+
let winnerTier = effectiveTier(winner);
|
|
40
|
+
for (let i = 1; i < candidates.length; i++) {
|
|
41
|
+
const candidate = candidates[i];
|
|
42
|
+
const candidateTier = effectiveTier(candidate);
|
|
43
|
+
const comparison = compareCandidates(winner, winnerTier, candidate, candidateTier);
|
|
44
|
+
if (comparison < 0) {
|
|
45
|
+
winner = candidate;
|
|
46
|
+
winnerTier = candidateTier;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const reason = buildReason(winner, winnerTier, candidates.length);
|
|
50
|
+
return ok({ winner, reason });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Consensus-based adjudicator for multi-agent scenarios.
|
|
55
|
+
*
|
|
56
|
+
* Gathers views from participating agents via ViewProvider,
|
|
57
|
+
* then applies majority voting with policy fallback.
|
|
58
|
+
* Requires a quorum (> 50% of views must agree).
|
|
59
|
+
*/
|
|
60
|
+
export class ConsensusAdjudicator {
|
|
61
|
+
viewProvider;
|
|
62
|
+
policyFallback = new PolicyAdjudicator();
|
|
63
|
+
constructor(viewProvider) {
|
|
64
|
+
this.viewProvider = viewProvider;
|
|
65
|
+
}
|
|
66
|
+
resolve(key, candidates) {
|
|
67
|
+
// Synchronous interface — consensus requires async gatherViews.
|
|
68
|
+
// For synchronous resolve(), fall back to policy adjudication.
|
|
69
|
+
// Use resolveAsync() for full consensus.
|
|
70
|
+
return this.policyFallback.resolve(key, candidates);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Async resolution with full multi-agent consensus.
|
|
74
|
+
*
|
|
75
|
+
* @param key - The contested key
|
|
76
|
+
* @param candidates - Local candidates
|
|
77
|
+
* @returns Consensus-resolved winner
|
|
78
|
+
*/
|
|
79
|
+
async resolveAsync(key, candidates) {
|
|
80
|
+
try {
|
|
81
|
+
const views = await this.viewProvider.gatherViews(key);
|
|
82
|
+
if (views.length === 0) {
|
|
83
|
+
return this.policyFallback.resolve(key, candidates);
|
|
84
|
+
}
|
|
85
|
+
// Combine local candidates with remote views
|
|
86
|
+
const allEntries = [
|
|
87
|
+
...candidates,
|
|
88
|
+
...views.map(v => v.entry),
|
|
89
|
+
];
|
|
90
|
+
if (allEntries.length === 0) {
|
|
91
|
+
return err(continuityError('NO_CANDIDATES', `No candidates for key "${key}"`));
|
|
92
|
+
}
|
|
93
|
+
// Count votes by value (group entries with same effective value)
|
|
94
|
+
const voteGroups = groupByValue(allEntries);
|
|
95
|
+
const totalVotes = allEntries.length;
|
|
96
|
+
const quorum = Math.floor(totalVotes / 2) + 1;
|
|
97
|
+
// Find group with most votes
|
|
98
|
+
let bestGroup = voteGroups[0];
|
|
99
|
+
for (const group of voteGroups) {
|
|
100
|
+
if (group.count > bestGroup.count) {
|
|
101
|
+
bestGroup = group;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (bestGroup.count >= quorum) {
|
|
105
|
+
// Quorum reached — use the best entry from the winning group
|
|
106
|
+
const groupResult = this.policyFallback.resolve(key, bestGroup.entries);
|
|
107
|
+
if (!groupResult.ok)
|
|
108
|
+
return groupResult;
|
|
109
|
+
return ok({
|
|
110
|
+
winner: groupResult.value.winner,
|
|
111
|
+
reason: `consensus: ${bestGroup.count}/${totalVotes} votes (quorum=${quorum})`,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
// No quorum — fall back to policy
|
|
115
|
+
return err(continuityError('QUORUM_NOT_MET', `Best group has ${bestGroup.count}/${totalVotes} votes, need ${quorum}`));
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
return err(continuityError('CONSENSUS_FAILED', `Consensus failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/* ── Internal helpers ── */
|
|
123
|
+
/**
|
|
124
|
+
* Compare two candidates using total ordering.
|
|
125
|
+
* Returns > 0 if a wins, < 0 if b wins, 0 if tied (shouldn't happen with DID tiebreaker).
|
|
126
|
+
*/
|
|
127
|
+
function compareCandidates(a, aTier, b, bTier) {
|
|
128
|
+
// 1. Higher tier wins
|
|
129
|
+
const tierDiff = TRUST_TIER_RANK[aTier] - TRUST_TIER_RANK[bTier];
|
|
130
|
+
if (tierDiff !== 0)
|
|
131
|
+
return tierDiff;
|
|
132
|
+
// 2. Newer timestamp wins
|
|
133
|
+
const aTime = a.provenance?.timestamp ?? 0;
|
|
134
|
+
const bTime = b.provenance?.timestamp ?? 0;
|
|
135
|
+
if (aTime !== bTime)
|
|
136
|
+
return aTime - bTime;
|
|
137
|
+
// 3. Lexicographically lowest author DID wins (deterministic tiebreaker)
|
|
138
|
+
const aAuthor = a.provenance?.author ?? '';
|
|
139
|
+
const bAuthor = b.provenance?.author ?? '';
|
|
140
|
+
if (aAuthor !== bAuthor) {
|
|
141
|
+
// Lower DID wins — so if a < b, a wins (return > 0)
|
|
142
|
+
return aAuthor < bAuthor ? 1 : -1;
|
|
143
|
+
}
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
function buildReason(winner, tier, candidateCount) {
|
|
147
|
+
const parts = [`tier=${tier}`];
|
|
148
|
+
if (winner.provenance) {
|
|
149
|
+
parts.push(`author=${winner.provenance.author.slice(0, 8)}...`);
|
|
150
|
+
parts.push(`ts=${winner.provenance.timestamp}`);
|
|
151
|
+
}
|
|
152
|
+
return `policy: selected from ${candidateCount} candidates (${parts.join(', ')})`;
|
|
153
|
+
}
|
|
154
|
+
function groupByValue(entries) {
|
|
155
|
+
const groups = new Map();
|
|
156
|
+
for (const entry of entries) {
|
|
157
|
+
const valueKey = canonicalValueKey(entry.value);
|
|
158
|
+
const group = groups.get(valueKey);
|
|
159
|
+
if (group) {
|
|
160
|
+
group.push(entry);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
groups.set(valueKey, [entry]);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return Array.from(groups.values()).map(entries => ({
|
|
167
|
+
entries,
|
|
168
|
+
count: entries.length,
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
function canonicalValueKey(value) {
|
|
172
|
+
if (value === null)
|
|
173
|
+
return 'null';
|
|
174
|
+
if (value instanceof Uint8Array) {
|
|
175
|
+
let s = 'bytes:';
|
|
176
|
+
for (let i = 0; i < value.length; i++)
|
|
177
|
+
s += String.fromCharCode(value[i]);
|
|
178
|
+
return s;
|
|
179
|
+
}
|
|
180
|
+
if (typeof value === 'object') {
|
|
181
|
+
return JSON.stringify(value, Object.keys(value).sort());
|
|
182
|
+
}
|
|
183
|
+
return `${typeof value}:${String(value)}`;
|
|
184
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @private.me/xcontinuity — Cascade / Sub-Agent Architecture
|
|
3
|
+
*
|
|
4
|
+
* Enables parent-child session hierarchies where child sessions
|
|
5
|
+
* inherit trust context from their parent. Proves that v2.0.0's
|
|
6
|
+
* trust substrate API composes correctly in multi-agent patterns.
|
|
7
|
+
*
|
|
8
|
+
* @since 2.1.0
|
|
9
|
+
*/
|
|
10
|
+
import type { Result } from '@private.me/shared';
|
|
11
|
+
import type { TrustTier, StateStore } from './types.js';
|
|
12
|
+
import type { ContinuityError } from './errors.js';
|
|
13
|
+
import { SessionManager } from './session.js';
|
|
14
|
+
import { TrustStore } from './ratification.js';
|
|
15
|
+
import type { MissionGuard } from './mission.js';
|
|
16
|
+
import { EnforcementLoop } from './enforcement.js';
|
|
17
|
+
/** How trust tiers propagate from parent to child sessions. */
|
|
18
|
+
export type TrustPropagation = 'inherit' | 'downgrade' | 'isolate';
|
|
19
|
+
/** Configuration for cascade trust behavior. */
|
|
20
|
+
export interface CascadePolicy {
|
|
21
|
+
/** How parent trust tiers propagate to children. Default: 'inherit'. */
|
|
22
|
+
readonly propagation: TrustPropagation;
|
|
23
|
+
/** Maximum trust tier a child can achieve. Default: 'ratified'. */
|
|
24
|
+
readonly maxChildTier: TrustTier;
|
|
25
|
+
/** Whether children can escalate violations to the parent. Default: true. */
|
|
26
|
+
readonly escalateToParent: boolean;
|
|
27
|
+
/** Maximum depth of the cascade tree. Default: 5. */
|
|
28
|
+
readonly maxDepth: number;
|
|
29
|
+
}
|
|
30
|
+
/** Default cascade policy. */
|
|
31
|
+
export declare const DEFAULT_CASCADE_POLICY: CascadePolicy;
|
|
32
|
+
/** Tracks a child session within a cascade. */
|
|
33
|
+
export interface CascadeChild {
|
|
34
|
+
readonly childId: string;
|
|
35
|
+
readonly session: SessionManager;
|
|
36
|
+
readonly trustStore: TrustStore;
|
|
37
|
+
readonly policy: CascadePolicy;
|
|
38
|
+
readonly depth: number;
|
|
39
|
+
readonly createdAt: number;
|
|
40
|
+
}
|
|
41
|
+
/** Escalation record from a child cascade. */
|
|
42
|
+
export interface CascadeEscalation {
|
|
43
|
+
readonly childId: string;
|
|
44
|
+
readonly agentId: string;
|
|
45
|
+
readonly violationCount: number;
|
|
46
|
+
readonly timestamp: number;
|
|
47
|
+
readonly action: import('./types.js').ProposedAction;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* CascadeSession wraps a SessionManager with parent-child hierarchy.
|
|
51
|
+
* Child sessions inherit trust context from their parent based on CascadePolicy.
|
|
52
|
+
*/
|
|
53
|
+
export declare class CascadeSession {
|
|
54
|
+
private readonly session;
|
|
55
|
+
private readonly trustStore;
|
|
56
|
+
private readonly missionGuard?;
|
|
57
|
+
private readonly enforcementLoop?;
|
|
58
|
+
private readonly children;
|
|
59
|
+
private readonly parentId?;
|
|
60
|
+
private readonly depth;
|
|
61
|
+
private readonly policy;
|
|
62
|
+
private readonly defaultMaxAge?;
|
|
63
|
+
private readonly escalations;
|
|
64
|
+
constructor(session: SessionManager, trustStore: TrustStore, options?: {
|
|
65
|
+
missionGuard?: MissionGuard;
|
|
66
|
+
enforcementLoop?: EnforcementLoop;
|
|
67
|
+
parentId?: string;
|
|
68
|
+
depth?: number;
|
|
69
|
+
policy?: CascadePolicy;
|
|
70
|
+
defaultMaxAge?: number;
|
|
71
|
+
});
|
|
72
|
+
/** Get the underlying session manager. */
|
|
73
|
+
getSession(): SessionManager;
|
|
74
|
+
/** Get the trust store for this cascade node. */
|
|
75
|
+
getTrustStore(): TrustStore;
|
|
76
|
+
/** Get the cascade depth (0 = root). */
|
|
77
|
+
getDepth(): number;
|
|
78
|
+
/** Get the parent session ID (undefined for root). */
|
|
79
|
+
getParentId(): string | undefined;
|
|
80
|
+
/** Get all child IDs. */
|
|
81
|
+
getChildIds(): string[];
|
|
82
|
+
/** Get a child by ID. */
|
|
83
|
+
getChild(childId: string): CascadeChild | undefined;
|
|
84
|
+
/** Get escalation records from child sessions. */
|
|
85
|
+
getEscalations(): readonly CascadeEscalation[];
|
|
86
|
+
/**
|
|
87
|
+
* Spawn a child session that inherits trust context from this parent.
|
|
88
|
+
*
|
|
89
|
+
* The child's trust store is populated based on the cascade policy:
|
|
90
|
+
* - 'inherit': Child receives parent's trust entries at their current tier.
|
|
91
|
+
* - 'downgrade': Child receives entries downgraded one tier level.
|
|
92
|
+
* - 'isolate': Child starts with an empty trust store.
|
|
93
|
+
*/
|
|
94
|
+
spawnChild(childAgentId: string, store: StateStore, childPolicy?: Partial<CascadePolicy>): Result<CascadeSession, ContinuityError>;
|
|
95
|
+
/**
|
|
96
|
+
* Merge a child's trusted state back into the parent.
|
|
97
|
+
* Only entries at or above 'inherited' tier are merged.
|
|
98
|
+
* The child session is closed after merging.
|
|
99
|
+
*/
|
|
100
|
+
mergeChild(childId: string): Result<number, ContinuityError>;
|
|
101
|
+
/**
|
|
102
|
+
* Close all children and then this session.
|
|
103
|
+
*/
|
|
104
|
+
closeAll(): void;
|
|
105
|
+
/** Number of active children. */
|
|
106
|
+
get childCount(): number;
|
|
107
|
+
private propagateTier;
|
|
108
|
+
private tierRank;
|
|
109
|
+
}
|
|
110
|
+
/** Configuration for the SubAgentCoordinator. */
|
|
111
|
+
export interface CoordinatorConfig {
|
|
112
|
+
/** Maximum number of concurrent sub-agents. Default: 10. */
|
|
113
|
+
readonly maxAgents: number;
|
|
114
|
+
/** Default cascade policy for spawned agents. */
|
|
115
|
+
readonly defaultPolicy: CascadePolicy;
|
|
116
|
+
/** Whether to auto-merge child state on completion. Default: false. */
|
|
117
|
+
readonly autoMerge: boolean;
|
|
118
|
+
}
|
|
119
|
+
/** Default coordinator configuration. */
|
|
120
|
+
export declare const DEFAULT_COORDINATOR_CONFIG: CoordinatorConfig;
|
|
121
|
+
/**
|
|
122
|
+
* SubAgentCoordinator manages the lifecycle of sub-agent sessions
|
|
123
|
+
* within a cascade hierarchy. It provides:
|
|
124
|
+
* - Spawning with trust delegation
|
|
125
|
+
* - State merging with enforcement checking
|
|
126
|
+
* - Lifecycle management (close, cleanup)
|
|
127
|
+
*/
|
|
128
|
+
export declare class SubAgentCoordinator {
|
|
129
|
+
private readonly root;
|
|
130
|
+
private readonly config;
|
|
131
|
+
private readonly activeAgents;
|
|
132
|
+
constructor(root: CascadeSession, config?: Partial<CoordinatorConfig>);
|
|
133
|
+
/** Get the root cascade session. */
|
|
134
|
+
getRoot(): CascadeSession;
|
|
135
|
+
/** Get the count of active sub-agents. */
|
|
136
|
+
get activeCount(): number;
|
|
137
|
+
/**
|
|
138
|
+
* Spawn a sub-agent with trust delegation from the root.
|
|
139
|
+
*/
|
|
140
|
+
spawn(agentId: string, store: StateStore, policy?: Partial<CascadePolicy>): Result<CascadeSession, ContinuityError>;
|
|
141
|
+
/**
|
|
142
|
+
* Complete a sub-agent and optionally merge its state back into the root.
|
|
143
|
+
*/
|
|
144
|
+
complete(childId: string): Result<number, ContinuityError>;
|
|
145
|
+
/**
|
|
146
|
+
* Merge a specific sub-agent's state into the root (manual merge).
|
|
147
|
+
*/
|
|
148
|
+
merge(childId: string): Result<number, ContinuityError>;
|
|
149
|
+
/**
|
|
150
|
+
* Close all sub-agents and the root session.
|
|
151
|
+
*/
|
|
152
|
+
shutdown(): void;
|
|
153
|
+
/**
|
|
154
|
+
* Get all active sub-agent IDs.
|
|
155
|
+
*/
|
|
156
|
+
getActiveIds(): string[];
|
|
157
|
+
}
|
package/dist/cascade.js
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @private.me/xcontinuity — Cascade / Sub-Agent Architecture
|
|
3
|
+
*
|
|
4
|
+
* Enables parent-child session hierarchies where child sessions
|
|
5
|
+
* inherit trust context from their parent. Proves that v2.0.0's
|
|
6
|
+
* trust substrate API composes correctly in multi-agent patterns.
|
|
7
|
+
*
|
|
8
|
+
* @since 2.1.0
|
|
9
|
+
*/
|
|
10
|
+
import { ok, err } from '@private.me/shared';
|
|
11
|
+
import { continuityError } from './errors.js';
|
|
12
|
+
import { SessionManager } from './session.js';
|
|
13
|
+
import { TrustStore } from './ratification.js';
|
|
14
|
+
import { EnforcementLoop } from './enforcement.js';
|
|
15
|
+
/** Default cascade policy. */
|
|
16
|
+
export const DEFAULT_CASCADE_POLICY = {
|
|
17
|
+
propagation: 'inherit',
|
|
18
|
+
maxChildTier: 'ratified',
|
|
19
|
+
escalateToParent: true,
|
|
20
|
+
maxDepth: 5,
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* CascadeSession wraps a SessionManager with parent-child hierarchy.
|
|
24
|
+
* Child sessions inherit trust context from their parent based on CascadePolicy.
|
|
25
|
+
*/
|
|
26
|
+
export class CascadeSession {
|
|
27
|
+
session;
|
|
28
|
+
trustStore;
|
|
29
|
+
missionGuard;
|
|
30
|
+
enforcementLoop;
|
|
31
|
+
children = new Map();
|
|
32
|
+
parentId;
|
|
33
|
+
depth;
|
|
34
|
+
policy;
|
|
35
|
+
defaultMaxAge;
|
|
36
|
+
escalations = [];
|
|
37
|
+
constructor(session, trustStore, options) {
|
|
38
|
+
this.session = session;
|
|
39
|
+
this.trustStore = trustStore;
|
|
40
|
+
this.missionGuard = options?.missionGuard;
|
|
41
|
+
this.enforcementLoop = options?.enforcementLoop;
|
|
42
|
+
this.parentId = options?.parentId;
|
|
43
|
+
this.depth = options?.depth ?? 0;
|
|
44
|
+
this.policy = options?.policy ?? DEFAULT_CASCADE_POLICY;
|
|
45
|
+
this.defaultMaxAge = options?.defaultMaxAge;
|
|
46
|
+
}
|
|
47
|
+
/** Get the underlying session manager. */
|
|
48
|
+
getSession() {
|
|
49
|
+
return this.session;
|
|
50
|
+
}
|
|
51
|
+
/** Get the trust store for this cascade node. */
|
|
52
|
+
getTrustStore() {
|
|
53
|
+
return this.trustStore;
|
|
54
|
+
}
|
|
55
|
+
/** Get the cascade depth (0 = root). */
|
|
56
|
+
getDepth() {
|
|
57
|
+
return this.depth;
|
|
58
|
+
}
|
|
59
|
+
/** Get the parent session ID (undefined for root). */
|
|
60
|
+
getParentId() {
|
|
61
|
+
return this.parentId;
|
|
62
|
+
}
|
|
63
|
+
/** Get all child IDs. */
|
|
64
|
+
getChildIds() {
|
|
65
|
+
return Array.from(this.children.keys());
|
|
66
|
+
}
|
|
67
|
+
/** Get a child by ID. */
|
|
68
|
+
getChild(childId) {
|
|
69
|
+
return this.children.get(childId);
|
|
70
|
+
}
|
|
71
|
+
/** Get escalation records from child sessions. */
|
|
72
|
+
getEscalations() {
|
|
73
|
+
return this.escalations;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Spawn a child session that inherits trust context from this parent.
|
|
77
|
+
*
|
|
78
|
+
* The child's trust store is populated based on the cascade policy:
|
|
79
|
+
* - 'inherit': Child receives parent's trust entries at their current tier.
|
|
80
|
+
* - 'downgrade': Child receives entries downgraded one tier level.
|
|
81
|
+
* - 'isolate': Child starts with an empty trust store.
|
|
82
|
+
*/
|
|
83
|
+
spawnChild(childAgentId, store, childPolicy) {
|
|
84
|
+
const mergedPolicy = { ...this.policy, ...childPolicy };
|
|
85
|
+
const childDepth = this.depth + 1;
|
|
86
|
+
if (childDepth > mergedPolicy.maxDepth) {
|
|
87
|
+
return err(continuityError('CONSTRAINT_VIOLATION', `Cascade depth ${childDepth} exceeds maximum ${mergedPolicy.maxDepth}`));
|
|
88
|
+
}
|
|
89
|
+
// Create child trust store
|
|
90
|
+
const childTrustStore = new TrustStore({
|
|
91
|
+
defaultMaxAge: this.defaultMaxAge,
|
|
92
|
+
});
|
|
93
|
+
// Propagate trust entries from parent to child
|
|
94
|
+
if (mergedPolicy.propagation !== 'isolate') {
|
|
95
|
+
const parentEntries = this.trustStore.all();
|
|
96
|
+
const propagated = [];
|
|
97
|
+
for (const entry of parentEntries) {
|
|
98
|
+
const childTier = this.propagateTier(entry.tier, mergedPolicy);
|
|
99
|
+
if (childTier && this.tierRank(childTier) <= this.tierRank(mergedPolicy.maxChildTier)) {
|
|
100
|
+
propagated.push({
|
|
101
|
+
key: entry.key,
|
|
102
|
+
value: entry.value,
|
|
103
|
+
provenance: entry.provenance,
|
|
104
|
+
tier: childTier,
|
|
105
|
+
contradictions: [],
|
|
106
|
+
maxAge: entry.maxAge,
|
|
107
|
+
ratifiedAt: childTier === 'ratified' ? entry.ratifiedAt : undefined,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (propagated.length > 0) {
|
|
112
|
+
childTrustStore.restore(propagated);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Create child enforcement loop that escalates to parent
|
|
116
|
+
let childEnforcement;
|
|
117
|
+
if (this.missionGuard) {
|
|
118
|
+
const escalateToParent = mergedPolicy.escalateToParent;
|
|
119
|
+
childEnforcement = new EnforcementLoop(this.missionGuard, {
|
|
120
|
+
escalationThreshold: 3,
|
|
121
|
+
onEscalate: escalateToParent
|
|
122
|
+
? (result) => {
|
|
123
|
+
this.escalations.push({
|
|
124
|
+
childId: childAgentId,
|
|
125
|
+
agentId: result.action.author,
|
|
126
|
+
violationCount: result.violationCount ?? 0,
|
|
127
|
+
timestamp: Date.now(),
|
|
128
|
+
action: result.action,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
: undefined,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
// Create child session
|
|
135
|
+
const childConfig = {
|
|
136
|
+
agentId: childAgentId,
|
|
137
|
+
store,
|
|
138
|
+
trustStore: childTrustStore,
|
|
139
|
+
missionGuard: this.missionGuard,
|
|
140
|
+
enforcementLoop: childEnforcement,
|
|
141
|
+
};
|
|
142
|
+
const childSession = SessionManager.create(childConfig);
|
|
143
|
+
const sessionId = childSession.session.sessionId;
|
|
144
|
+
const cascadeChild = new CascadeSession(childSession, childTrustStore, {
|
|
145
|
+
missionGuard: this.missionGuard,
|
|
146
|
+
enforcementLoop: childEnforcement,
|
|
147
|
+
parentId: this.session.session.sessionId,
|
|
148
|
+
depth: childDepth,
|
|
149
|
+
policy: mergedPolicy,
|
|
150
|
+
defaultMaxAge: this.defaultMaxAge,
|
|
151
|
+
});
|
|
152
|
+
this.children.set(sessionId, {
|
|
153
|
+
childId: sessionId,
|
|
154
|
+
session: childSession,
|
|
155
|
+
trustStore: childTrustStore,
|
|
156
|
+
policy: mergedPolicy,
|
|
157
|
+
depth: childDepth,
|
|
158
|
+
createdAt: Date.now(),
|
|
159
|
+
});
|
|
160
|
+
return ok(cascadeChild);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Merge a child's trusted state back into the parent.
|
|
164
|
+
* Only entries at or above 'inherited' tier are merged.
|
|
165
|
+
* The child session is closed after merging.
|
|
166
|
+
*/
|
|
167
|
+
mergeChild(childId) {
|
|
168
|
+
const child = this.children.get(childId);
|
|
169
|
+
if (!child) {
|
|
170
|
+
return err(continuityError('SNAPSHOT_NOT_FOUND', `Child session "${childId}" not found`));
|
|
171
|
+
}
|
|
172
|
+
let merged = 0;
|
|
173
|
+
const childEntries = child.trustStore.all();
|
|
174
|
+
for (const entry of childEntries) {
|
|
175
|
+
if (this.tierRank(entry.tier) >= this.tierRank('inherited')) {
|
|
176
|
+
// Check parent enforcement before merging
|
|
177
|
+
if (this.enforcementLoop) {
|
|
178
|
+
const check = this.enforcementLoop.check({
|
|
179
|
+
author: child.session.session.agentId,
|
|
180
|
+
type: 'write',
|
|
181
|
+
key: entry.key,
|
|
182
|
+
value: entry.value,
|
|
183
|
+
});
|
|
184
|
+
if (check.ok && check.value.decision !== 'allow') {
|
|
185
|
+
continue; // Skip entries that violate parent constraints
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
this.trustStore.write(entry.key, entry.value, entry.provenance);
|
|
189
|
+
merged++;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Close child session
|
|
193
|
+
child.session.close();
|
|
194
|
+
this.children.delete(childId);
|
|
195
|
+
return ok(merged);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Close all children and then this session.
|
|
199
|
+
*/
|
|
200
|
+
closeAll() {
|
|
201
|
+
for (const [, child] of this.children) {
|
|
202
|
+
child.session.close();
|
|
203
|
+
}
|
|
204
|
+
this.children.clear();
|
|
205
|
+
this.session.close();
|
|
206
|
+
}
|
|
207
|
+
/** Number of active children. */
|
|
208
|
+
get childCount() {
|
|
209
|
+
return this.children.size;
|
|
210
|
+
}
|
|
211
|
+
propagateTier(parentTier, policy) {
|
|
212
|
+
if (policy.propagation === 'isolate')
|
|
213
|
+
return null;
|
|
214
|
+
if (policy.propagation === 'inherit')
|
|
215
|
+
return parentTier;
|
|
216
|
+
// Downgrade: ratified -> inherited, inherited -> quarantined, quarantined -> null
|
|
217
|
+
if (parentTier === 'ratified')
|
|
218
|
+
return 'inherited';
|
|
219
|
+
if (parentTier === 'inherited')
|
|
220
|
+
return 'quarantined';
|
|
221
|
+
return null; // quarantined cannot be downgraded further
|
|
222
|
+
}
|
|
223
|
+
tierRank(tier) {
|
|
224
|
+
const ranks = {
|
|
225
|
+
ratified: 3,
|
|
226
|
+
inherited: 2,
|
|
227
|
+
quarantined: 1,
|
|
228
|
+
};
|
|
229
|
+
return ranks[tier];
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/** Default coordinator configuration. */
|
|
233
|
+
export const DEFAULT_COORDINATOR_CONFIG = {
|
|
234
|
+
maxAgents: 10,
|
|
235
|
+
defaultPolicy: DEFAULT_CASCADE_POLICY,
|
|
236
|
+
autoMerge: false,
|
|
237
|
+
};
|
|
238
|
+
/**
|
|
239
|
+
* SubAgentCoordinator manages the lifecycle of sub-agent sessions
|
|
240
|
+
* within a cascade hierarchy. It provides:
|
|
241
|
+
* - Spawning with trust delegation
|
|
242
|
+
* - State merging with enforcement checking
|
|
243
|
+
* - Lifecycle management (close, cleanup)
|
|
244
|
+
*/
|
|
245
|
+
export class SubAgentCoordinator {
|
|
246
|
+
root;
|
|
247
|
+
config;
|
|
248
|
+
activeAgents = new Map();
|
|
249
|
+
constructor(root, config) {
|
|
250
|
+
this.root = root;
|
|
251
|
+
this.config = { ...DEFAULT_COORDINATOR_CONFIG, ...config };
|
|
252
|
+
}
|
|
253
|
+
/** Get the root cascade session. */
|
|
254
|
+
getRoot() {
|
|
255
|
+
return this.root;
|
|
256
|
+
}
|
|
257
|
+
/** Get the count of active sub-agents. */
|
|
258
|
+
get activeCount() {
|
|
259
|
+
return this.activeAgents.size;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Spawn a sub-agent with trust delegation from the root.
|
|
263
|
+
*/
|
|
264
|
+
spawn(agentId, store, policy) {
|
|
265
|
+
if (this.activeAgents.size >= this.config.maxAgents) {
|
|
266
|
+
return err(continuityError('CONSTRAINT_VIOLATION', `Maximum sub-agents (${this.config.maxAgents}) reached`));
|
|
267
|
+
}
|
|
268
|
+
const mergedPolicy = { ...this.config.defaultPolicy, ...policy };
|
|
269
|
+
const result = this.root.spawnChild(agentId, store, mergedPolicy);
|
|
270
|
+
if (!result.ok)
|
|
271
|
+
return result;
|
|
272
|
+
const child = result.value;
|
|
273
|
+
const childId = child.getSession().session.sessionId;
|
|
274
|
+
this.activeAgents.set(childId, child);
|
|
275
|
+
return ok(child);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Complete a sub-agent and optionally merge its state back into the root.
|
|
279
|
+
*/
|
|
280
|
+
complete(childId) {
|
|
281
|
+
const child = this.activeAgents.get(childId);
|
|
282
|
+
if (!child) {
|
|
283
|
+
return err(continuityError('SNAPSHOT_NOT_FOUND', `Sub-agent "${childId}" not found in coordinator`));
|
|
284
|
+
}
|
|
285
|
+
let merged = 0;
|
|
286
|
+
if (this.config.autoMerge) {
|
|
287
|
+
const mergeResult = this.root.mergeChild(childId);
|
|
288
|
+
if (mergeResult.ok)
|
|
289
|
+
merged = mergeResult.value;
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
child.getSession().close();
|
|
293
|
+
}
|
|
294
|
+
this.activeAgents.delete(childId);
|
|
295
|
+
return ok(merged);
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Merge a specific sub-agent's state into the root (manual merge).
|
|
299
|
+
*/
|
|
300
|
+
merge(childId) {
|
|
301
|
+
if (!this.activeAgents.has(childId)) {
|
|
302
|
+
return err(continuityError('SNAPSHOT_NOT_FOUND', `Sub-agent "${childId}" not found in coordinator`));
|
|
303
|
+
}
|
|
304
|
+
const result = this.root.mergeChild(childId);
|
|
305
|
+
if (result.ok) {
|
|
306
|
+
this.activeAgents.delete(childId);
|
|
307
|
+
}
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Close all sub-agents and the root session.
|
|
312
|
+
*/
|
|
313
|
+
shutdown() {
|
|
314
|
+
this.activeAgents.clear();
|
|
315
|
+
this.root.closeAll();
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Get all active sub-agent IDs.
|
|
319
|
+
*/
|
|
320
|
+
getActiveIds() {
|
|
321
|
+
return Array.from(this.activeAgents.keys());
|
|
322
|
+
}
|
|
323
|
+
}
|