@capekai/core 1.0.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/README.md +12 -0
- package/package.json +105 -0
- package/src/adapters/ai-sdk.ts +84 -0
- package/src/compaction/contracts.ts +82 -0
- package/src/compaction/executor.ts +161 -0
- package/src/compaction/policy.ts +318 -0
- package/src/compaction/recovery.ts +139 -0
- package/src/compaction/task.ts +540 -0
- package/src/configuration/contracts.ts +58 -0
- package/src/configuration/defaults.ts +27 -0
- package/src/configuration/runtime.ts +42 -0
- package/src/configuration/single-model.ts +75 -0
- package/src/context/assembler.ts +112 -0
- package/src/context/index.ts +2 -0
- package/src/context/sources.ts +119 -0
- package/src/context/workspace.ts +63 -0
- package/src/core/agent.ts +401 -0
- package/src/core/build-tools.ts +139 -0
- package/src/core/chat-handler.ts +858 -0
- package/src/core/error-handling.ts +18 -0
- package/src/core/fork.ts +103 -0
- package/src/core/interrupt.ts +192 -0
- package/src/core/message-utils.ts +261 -0
- package/src/core/model-utils.ts +149 -0
- package/src/core/part-utils.ts +88 -0
- package/src/core/provider-utils.ts +67 -0
- package/src/core/revert.ts +46 -0
- package/src/core/step-handlers.ts +157 -0
- package/src/core/stream/finalization.ts +65 -0
- package/src/core/stream/stream-config.ts +82 -0
- package/src/core/stream-handlers.ts +242 -0
- package/src/core/structured-output.ts +68 -0
- package/src/core/tool-builders/agent-tools.ts +71 -0
- package/src/core/tool-builders/external-tools.ts +179 -0
- package/src/core/tool-builders/types.ts +16 -0
- package/src/core/tool-builders/workspace-tools.ts +293 -0
- package/src/core/tool-capabilities.ts +65 -0
- package/src/goals/evaluator.ts +171 -0
- package/src/goals/index.ts +3 -0
- package/src/goals/loop.ts +167 -0
- package/src/goals/service.ts +39 -0
- package/src/index.ts +10 -0
- package/src/internal/ask-authority.ts +29 -0
- package/src/internal/composition.ts +44 -0
- package/src/internal/configuration.ts +22 -0
- package/src/internal/execution.ts +108 -0
- package/src/internal/hosts.ts +64 -0
- package/src/internal/plugins.ts +71 -0
- package/src/internal/providers.ts +32 -0
- package/src/internal/sandbox.ts +19 -0
- package/src/internal/tools.ts +48 -0
- package/src/internal/workspace.ts +25 -0
- package/src/kernel/diagnostics.ts +249 -0
- package/src/kernel/errors.ts +120 -0
- package/src/kernel/events.ts +82 -0
- package/src/kernel/index.ts +72 -0
- package/src/kernel/kernel.ts +62 -0
- package/src/kernel/lifecycle.ts +72 -0
- package/src/kernel/plugin.ts +218 -0
- package/src/kernel/registry.ts +493 -0
- package/src/kernel/scope.ts +776 -0
- package/src/kernel/service-key.ts +19 -0
- package/src/kernel/types.ts +317 -0
- package/src/memory/index.ts +2 -0
- package/src/memory/memory-tool.ts +75 -0
- package/src/memory/registry.ts +172 -0
- package/src/permission/ask-user-api.ts +70 -0
- package/src/permission/contracts.ts +135 -0
- package/src/permission/permission-request-manager.ts +58 -0
- package/src/permission/policy.ts +277 -0
- package/src/permission/runtime.ts +612 -0
- package/src/plugins/compaction-policy.ts +46 -0
- package/src/plugins/compose.ts +171 -0
- package/src/plugins/context-sections.ts +246 -0
- package/src/plugins/default-agent-driver.ts +14 -0
- package/src/plugins/facade-plugins.ts +129 -0
- package/src/plugins/goal-domain.ts +82 -0
- package/src/plugins/legacy-system-message.ts +152 -0
- package/src/plugins/loaded-tools.ts +23 -0
- package/src/plugins/memory-domain.ts +264 -0
- package/src/plugins/orchestrator-session.ts +29 -0
- package/src/plugins/permission-policy.ts +49 -0
- package/src/plugins/retry-policy.ts +28 -0
- package/src/plugins/scheduler-domain.ts +192 -0
- package/src/plugins/service-keys.ts +294 -0
- package/src/plugins/session-search-domain.ts +238 -0
- package/src/plugins/skills-domain.ts +272 -0
- package/src/plugins/subagent-domain.ts +287 -0
- package/src/plugins/tool-catalog.ts +78 -0
- package/src/plugins/tool-output-policy.ts +52 -0
- package/src/plugins/value-plugins.ts +150 -0
- package/src/plugins/workflow-domain.ts +198 -0
- package/src/plugins/workspace-policy.ts +37 -0
- package/src/providers/registry.ts +63 -0
- package/src/providers/types.ts +44 -0
- package/src/retry/policy.ts +282 -0
- package/src/retry/stream-chat.ts +312 -0
- package/src/runtime/agent-runtime.ts +83 -0
- package/src/runtime/default-agent-driver.ts +23 -0
- package/src/runtime/domain-tool-source.ts +156 -0
- package/src/runtime/events.ts +61 -0
- package/src/runtime/host-dependencies.ts +71 -0
- package/src/runtime/host-guidance.ts +22 -0
- package/src/runtime/host-layout.ts +23 -0
- package/src/runtime/host.ts +129 -0
- package/src/runtime/standalone-host.ts +118 -0
- package/src/sandbox/controller.ts +204 -0
- package/src/sandbox/model.ts +305 -0
- package/src/sandbox/provider.ts +53 -0
- package/src/sandbox/types.ts +110 -0
- package/src/scheduler/host.ts +22 -0
- package/src/scheduler/scheduler-tool.ts +172 -0
- package/src/session-search/host.ts +56 -0
- package/src/session-search/index.ts +23 -0
- package/src/session-search/session-search-tool.ts +151 -0
- package/src/skills/index.ts +3 -0
- package/src/skills/registry.ts +63 -0
- package/src/skills/skill-manage-tool.ts +205 -0
- package/src/skills/skill-tool.ts +42 -0
- package/src/storage/contracts.ts +159 -0
- package/src/storage/memory.ts +321 -0
- package/src/storage/options.ts +75 -0
- package/src/storage/runtime.ts +115 -0
- package/src/storage/sqlite-tool-output-artifacts.ts +106 -0
- package/src/storage/sqlite.ts +321 -0
- package/src/storage/tool-output-artifacts.ts +75 -0
- package/src/storage.ts +31 -0
- package/src/subagent/child-session.ts +282 -0
- package/src/subagent/guidance.ts +8 -0
- package/src/subagent/policy.ts +198 -0
- package/src/subagent/task-tool.ts +584 -0
- package/src/tool-output/contracts.ts +111 -0
- package/src/tool-output/policy.ts +410 -0
- package/src/tool.ts +1 -0
- package/src/tools/executor.ts +258 -0
- package/src/tools/install-manifest.ts +40 -0
- package/src/tools/llm-api.ts +77 -0
- package/src/tools/registry.ts +206 -0
- package/src/tools/tool-artifact.ts +182 -0
- package/src/tools/tool-source.ts +53 -0
- package/src/utils/errors.ts +334 -0
- package/src/utils/strip-visualization.ts +50 -0
- package/src/workflow/decomposer.ts +139 -0
- package/src/workflow/execution.ts +523 -0
- package/src/workflow/orchestrator-session.ts +161 -0
- package/src/workflow/synthesizer.ts +130 -0
- package/src/workspace/contracts.ts +135 -0
- package/src/workspace/policy.ts +327 -0
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scope nodes. A process scope holds process plugins; agent scopes extend
|
|
3
|
+
* it; run scopes extend an agent scope. Resolution order is run, agent,
|
|
4
|
+
* process for services and contributions. Disposal disposes children in
|
|
5
|
+
* reverse creation order, awaits registered cleanup barriers, then
|
|
6
|
+
* disposes plugins in reverse activation order.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
DuplicateContributionError,
|
|
11
|
+
DuplicateProviderError,
|
|
12
|
+
LifecycleError,
|
|
13
|
+
MalformedPluginError,
|
|
14
|
+
RunTerminalError,
|
|
15
|
+
ScopeValidationError,
|
|
16
|
+
type DisposalFailure,
|
|
17
|
+
} from './errors';
|
|
18
|
+
import {
|
|
19
|
+
CONTEXT_PHASES,
|
|
20
|
+
buildContextSections,
|
|
21
|
+
buildSnapshot,
|
|
22
|
+
collectEffectiveContextSections,
|
|
23
|
+
collectEffectiveTools,
|
|
24
|
+
type LocalContextSectionRegistration,
|
|
25
|
+
type LocalListenerRegistration,
|
|
26
|
+
type LocalToolRegistration,
|
|
27
|
+
type ResolvedService,
|
|
28
|
+
} from './diagnostics';
|
|
29
|
+
import { dispatchEvent, validateListener, type ListenerRegistration } from './events';
|
|
30
|
+
import { activateRecords, disposeRecords, throwDisposalError } from './lifecycle';
|
|
31
|
+
import { createPluginRecords, type PluginContextHost, type PluginRecord } from './plugin';
|
|
32
|
+
import { planActivation, type ParentServiceInfo } from './registry';
|
|
33
|
+
import type {
|
|
34
|
+
CapekPlugin,
|
|
35
|
+
CleanupBarrier,
|
|
36
|
+
ContextSectionContribution,
|
|
37
|
+
Disposable,
|
|
38
|
+
EffectiveContextSection,
|
|
39
|
+
EffectiveTool,
|
|
40
|
+
EventListenerContribution,
|
|
41
|
+
KernelEvent,
|
|
42
|
+
PluginOptionsMap,
|
|
43
|
+
ProvidedContextSection,
|
|
44
|
+
RunCancellation,
|
|
45
|
+
RunStatus,
|
|
46
|
+
RunTerminalOutcome,
|
|
47
|
+
RuntimeScope,
|
|
48
|
+
ScopeDiagnosticsSnapshot,
|
|
49
|
+
ScopeStatus,
|
|
50
|
+
ServiceKey,
|
|
51
|
+
ToolContribution,
|
|
52
|
+
} from './types';
|
|
53
|
+
|
|
54
|
+
let scopeCounter = 0;
|
|
55
|
+
|
|
56
|
+
function nextScopeId(kind: RuntimeScope): string {
|
|
57
|
+
scopeCounter += 1;
|
|
58
|
+
return `${kind}:${scopeCounter}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Brands registered in the global symbol registry so a parent scope
|
|
62
|
+
* created through a duplicate module instance still carries the same
|
|
63
|
+
* brand. The check validates the parent link structurally across module
|
|
64
|
+
* instances; it is not a security boundary against code that can read the
|
|
65
|
+
* global symbol registry. */
|
|
66
|
+
export const processScopeBrand: unique symbol = Symbol.for('capek.kernel.process-scope');
|
|
67
|
+
export const agentScopeBrand: unique symbol = Symbol.for('capek.kernel.agent-scope');
|
|
68
|
+
|
|
69
|
+
interface ServiceRegistration {
|
|
70
|
+
readonly key: ServiceKey<unknown>;
|
|
71
|
+
readonly value: unknown;
|
|
72
|
+
readonly providerPluginId: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface BarrierRegistration {
|
|
76
|
+
readonly fn: () => PromiseLike<void>;
|
|
77
|
+
readonly pluginId: string;
|
|
78
|
+
removed: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type ContributionStoreKey = 'tool' | 'context' | 'listener';
|
|
82
|
+
|
|
83
|
+
function validateContributionObject(kind: string, value: unknown): asserts value is object {
|
|
84
|
+
if (typeof value !== 'object' || value === null) {
|
|
85
|
+
throw new MalformedPluginError(`${kind} contribution must be an object`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function validateContributionId(kind: string, id: unknown): asserts id is string {
|
|
90
|
+
if (typeof id !== 'string' || id.length === 0) {
|
|
91
|
+
throw new MalformedPluginError(`${kind} contribution id must be a non-empty string`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function validateFiniteOrder(kind: string, id: string, order: unknown): asserts order is number {
|
|
96
|
+
if (typeof order !== 'number' || !Number.isFinite(order)) {
|
|
97
|
+
throw new MalformedPluginError(`${kind} contribution '${id}' must have a finite order`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const VALID_KEY_SCOPES: ReadonlySet<string> = new Set(['process', 'agent', 'run']);
|
|
102
|
+
|
|
103
|
+
function validateServiceKeyShape(
|
|
104
|
+
kind: string,
|
|
105
|
+
contributionId: string,
|
|
106
|
+
key: unknown,
|
|
107
|
+
): asserts key is ServiceKey<unknown> {
|
|
108
|
+
if (typeof key !== 'object' || key === null) {
|
|
109
|
+
throw new MalformedPluginError(
|
|
110
|
+
`${kind} contribution '${contributionId}' requiredCapabilities must contain only service keys`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const candidate = key as Partial<ServiceKey<unknown>>;
|
|
114
|
+
if (typeof candidate.id !== 'string' || candidate.id.length === 0) {
|
|
115
|
+
throw new MalformedPluginError(
|
|
116
|
+
`${kind} contribution '${contributionId}' requiredCapabilities contains a service key with a non-string or empty id`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
if (!VALID_KEY_SCOPES.has(candidate.scope as string)) {
|
|
120
|
+
throw new MalformedPluginError(
|
|
121
|
+
`${kind} contribution '${contributionId}' requiredCapabilities contains service key '${candidate.id}' with invalid scope '${String(candidate.scope)}'`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
function validateToolContribution(contribution: ToolContribution): void {
|
|
128
|
+
validateContributionObject('tool', contribution);
|
|
129
|
+
validateContributionId('tool', contribution.id);
|
|
130
|
+
if (
|
|
131
|
+
contribution.order !== undefined
|
|
132
|
+
&& (typeof contribution.order !== 'number' || !Number.isFinite(contribution.order))
|
|
133
|
+
) {
|
|
134
|
+
throw new MalformedPluginError(
|
|
135
|
+
`tool contribution '${contribution.id}' must have a finite order when present`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
const definition = contribution.definition;
|
|
139
|
+
if (typeof definition !== 'object' || definition === null) {
|
|
140
|
+
throw new MalformedPluginError(
|
|
141
|
+
`tool contribution '${contribution.id}' must carry an object definition`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (typeof definition.name !== 'string' || definition.name.length === 0) {
|
|
145
|
+
throw new MalformedPluginError(
|
|
146
|
+
`tool contribution '${contribution.id}' definition name must be a non-empty string`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
const requiredCapabilities = contribution.requiredCapabilities;
|
|
150
|
+
if (requiredCapabilities !== undefined && requiredCapabilities !== null) {
|
|
151
|
+
if (!Array.isArray(requiredCapabilities)) {
|
|
152
|
+
throw new MalformedPluginError(
|
|
153
|
+
`tool contribution '${contribution.id}' requiredCapabilities must be an array of service keys`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
for (const key of requiredCapabilities) {
|
|
157
|
+
validateServiceKeyShape('tool', contribution.id, key);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const visibility = contribution.visibility;
|
|
161
|
+
if (visibility !== undefined && visibility !== null) {
|
|
162
|
+
if (typeof visibility !== 'object') {
|
|
163
|
+
throw new MalformedPluginError(
|
|
164
|
+
`tool contribution '${contribution.id}' visibility must be an object`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
if (typeof (visibility as { visible?: unknown }).visible !== 'boolean') {
|
|
168
|
+
throw new MalformedPluginError(
|
|
169
|
+
`tool contribution '${contribution.id}' visibility must declare a boolean visible`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
const reason = (visibility as { reason?: unknown }).reason;
|
|
173
|
+
if (reason !== undefined && typeof reason !== 'string') {
|
|
174
|
+
throw new MalformedPluginError(
|
|
175
|
+
`tool contribution '${contribution.id}' visibility reason must be a string when present`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
async function activateScope(
|
|
183
|
+
scope: ScopeBase,
|
|
184
|
+
plugins: readonly CapekPlugin<unknown>[],
|
|
185
|
+
options: PluginOptionsMap,
|
|
186
|
+
): Promise<void> {
|
|
187
|
+
const parentServices = scope.parent === null
|
|
188
|
+
? new Map<string, ParentServiceInfo>()
|
|
189
|
+
: scope.parent.effectiveServiceInfos();
|
|
190
|
+
const plan = planActivation(scope.kind, plugins, parentServices);
|
|
191
|
+
const records = createPluginRecords(scope, plugins);
|
|
192
|
+
scope.pluginRecords.push(...plan.order.map((id) => records.get(id) as PluginRecord));
|
|
193
|
+
await activateRecords(scope.pluginRecords, plan.order, options);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export abstract class ScopeBase implements PluginContextHost {
|
|
197
|
+
abstract readonly kind: RuntimeScope;
|
|
198
|
+
readonly scopeId: string;
|
|
199
|
+
readonly parent: ScopeBase | null;
|
|
200
|
+
status: ScopeStatus = 'active';
|
|
201
|
+
readonly pluginRecords: PluginRecord[] = [];
|
|
202
|
+
|
|
203
|
+
private readonly services = new Map<string, ServiceRegistration>();
|
|
204
|
+
private readonly tools = new Map<string, LocalToolRegistration>();
|
|
205
|
+
private readonly contextSections = new Map<string, LocalContextSectionRegistration>();
|
|
206
|
+
private readonly listeners = new Map<string, ListenerRegistration>();
|
|
207
|
+
private readonly barriers: BarrierRegistration[] = [];
|
|
208
|
+
private readonly children: ScopeBase[] = [];
|
|
209
|
+
private disposePromise: Promise<void> | null = null;
|
|
210
|
+
|
|
211
|
+
protected constructor(parent: ScopeBase | null, kind: RuntimeScope, scopeId?: string) {
|
|
212
|
+
this.parent = parent;
|
|
213
|
+
this.scopeId = scopeId ?? nextScopeId(kind);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Host surface used by plugin contexts.
|
|
217
|
+
|
|
218
|
+
registerService(
|
|
219
|
+
pluginId: string,
|
|
220
|
+
key: ServiceKey<unknown>,
|
|
221
|
+
value: unknown,
|
|
222
|
+
replacesProvider?: string,
|
|
223
|
+
): Disposable {
|
|
224
|
+
const existing = this.services.get(key.id);
|
|
225
|
+
if (existing !== undefined && existing.providerPluginId !== replacesProvider) {
|
|
226
|
+
throw new DuplicateProviderError(
|
|
227
|
+
`service '${key.id}' is already provided by plugin '${existing.providerPluginId}' in this ${this.kind} scope`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
const record: ServiceRegistration = { key, value, providerPluginId: pluginId };
|
|
231
|
+
this.services.set(key.id, record);
|
|
232
|
+
let removed = false;
|
|
233
|
+
return {
|
|
234
|
+
dispose: () => {
|
|
235
|
+
if (removed) return;
|
|
236
|
+
removed = true;
|
|
237
|
+
if (this.services.get(key.id) === record) {
|
|
238
|
+
this.services.delete(key.id);
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
registerTool(pluginId: string, contribution: ToolContribution): Disposable {
|
|
245
|
+
validateToolContribution(contribution);
|
|
246
|
+
const existing = this.findContributionInChain('tool', contribution.id);
|
|
247
|
+
if (existing !== null) {
|
|
248
|
+
throw new DuplicateContributionError(
|
|
249
|
+
`tool contribution '${contribution.id}' is already registered in the ${existing.scopeKind} scope by plugin '${existing.pluginId}'`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
this.tools.set(contribution.id, { contribution, pluginId });
|
|
253
|
+
let removed = false;
|
|
254
|
+
return {
|
|
255
|
+
dispose: () => {
|
|
256
|
+
if (removed) return;
|
|
257
|
+
removed = true;
|
|
258
|
+
this.tools.delete(contribution.id);
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
registerContextSection(
|
|
264
|
+
pluginId: string,
|
|
265
|
+
contribution: ContextSectionContribution,
|
|
266
|
+
): Disposable {
|
|
267
|
+
validateContributionObject('context section', contribution);
|
|
268
|
+
validateContributionId('context section', contribution.id);
|
|
269
|
+
validateFiniteOrder('context section', contribution.id, contribution.order);
|
|
270
|
+
if (!(CONTEXT_PHASES as readonly unknown[]).includes(contribution.phase)) {
|
|
271
|
+
throw new MalformedPluginError(
|
|
272
|
+
`context section '${contribution.id}' has unknown phase '${String(contribution.phase)}'`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
if (typeof contribution.provide !== 'function') {
|
|
276
|
+
throw new MalformedPluginError(
|
|
277
|
+
`context section '${contribution.id}' must provide a provide function`,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
const existing = this.findContributionInChain('context', contribution.id);
|
|
281
|
+
if (existing !== null) {
|
|
282
|
+
throw new DuplicateContributionError(
|
|
283
|
+
`context section '${contribution.id}' is already registered in the ${existing.scopeKind} scope by plugin '${existing.pluginId}'`,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
this.contextSections.set(contribution.id, { contribution, pluginId });
|
|
287
|
+
let removed = false;
|
|
288
|
+
return {
|
|
289
|
+
dispose: () => {
|
|
290
|
+
if (removed) return;
|
|
291
|
+
removed = true;
|
|
292
|
+
this.contextSections.delete(contribution.id);
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
registerListener(pluginId: string, contribution: EventListenerContribution): Disposable {
|
|
298
|
+
validateListener(contribution);
|
|
299
|
+
const existing = this.findContributionInChain('listener', contribution.id);
|
|
300
|
+
if (existing !== null) {
|
|
301
|
+
throw new DuplicateContributionError(
|
|
302
|
+
`listener '${contribution.id}' is already registered in the ${existing.scopeKind} scope by plugin '${existing.pluginId}'`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
this.listeners.set(contribution.id, { contribution, pluginId });
|
|
306
|
+
let removed = false;
|
|
307
|
+
return {
|
|
308
|
+
dispose: () => {
|
|
309
|
+
if (removed) return;
|
|
310
|
+
removed = true;
|
|
311
|
+
this.listeners.delete(contribution.id);
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
/** Walks the scope chain so a child scope can never register an id that is
|
|
318
|
+
* already contributed by any ancestor. Distinct ancestor ids stay inherited. */
|
|
319
|
+
private findContributionInChain(
|
|
320
|
+
kind: ContributionStoreKey,
|
|
321
|
+
id: string,
|
|
322
|
+
): { pluginId: string; scopeKind: RuntimeScope } | null {
|
|
323
|
+
const local = this.contributionMapFor(kind).get(id);
|
|
324
|
+
if (local !== undefined) {
|
|
325
|
+
return { pluginId: local.pluginId, scopeKind: this.kind };
|
|
326
|
+
}
|
|
327
|
+
return this.parent === null
|
|
328
|
+
? null
|
|
329
|
+
: this.parent.findContributionInChain(kind, id);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private contributionMapFor(kind: ContributionStoreKey): ReadonlyMap<string, { pluginId: string }> {
|
|
333
|
+
switch (kind) {
|
|
334
|
+
case 'tool': return this.tools;
|
|
335
|
+
case 'context': return this.contextSections;
|
|
336
|
+
case 'listener': return this.listeners;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
registerBarrier(pluginId: string, barrier: CleanupBarrier): Disposable {
|
|
341
|
+
const fn = typeof barrier === 'function' ? barrier : () => barrier;
|
|
342
|
+
const registration: BarrierRegistration = { fn, pluginId, removed: false };
|
|
343
|
+
this.barriers.push(registration);
|
|
344
|
+
return {
|
|
345
|
+
dispose: () => {
|
|
346
|
+
registration.removed = true;
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Service resolution across the scope chain.
|
|
352
|
+
|
|
353
|
+
resolveService<T>(key: ServiceKey<T>): T | undefined {
|
|
354
|
+
const found = this.findServiceRegistration(key.id);
|
|
355
|
+
if (found === null) return undefined;
|
|
356
|
+
if (found.registration.key.scope !== key.scope) {
|
|
357
|
+
throw new ScopeValidationError(
|
|
358
|
+
`service '${key.id}' is provided with scope '${found.registration.key.scope}' by plugin '${found.registration.providerPluginId}' in the ${found.scopeKind} scope but was requested with scope '${key.scope}'; conflicting ServiceKey scopes cannot resolve`,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
return found.registration.value as T;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
resolveServiceRecord(keyId: string): ResolvedService | null {
|
|
365
|
+
const found = this.findServiceRegistration(keyId);
|
|
366
|
+
if (found === null) return null;
|
|
367
|
+
return {
|
|
368
|
+
key: found.registration.key,
|
|
369
|
+
providerPluginId: found.registration.providerPluginId,
|
|
370
|
+
providerScope: found.scopeKind,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private findServiceRegistration(keyId: string): {
|
|
375
|
+
registration: ServiceRegistration;
|
|
376
|
+
scopeKind: RuntimeScope;
|
|
377
|
+
} | null {
|
|
378
|
+
const local = this.services.get(keyId);
|
|
379
|
+
if (local !== undefined) {
|
|
380
|
+
return { registration: local, scopeKind: this.kind };
|
|
381
|
+
}
|
|
382
|
+
return this.parent === null ? null : this.parent.findServiceRegistration(keyId);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
require<T>(key: ServiceKey<T>): T {
|
|
386
|
+
const resolved = this.resolveService(key);
|
|
387
|
+
if (resolved === undefined) {
|
|
388
|
+
throw new LifecycleError(
|
|
389
|
+
`service '${key.id}' is not available in the current scope chain`,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
return resolved;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
optional<T>(key: ServiceKey<T>): T | undefined {
|
|
396
|
+
return this.resolveService(key);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Diagnostics and listings.
|
|
400
|
+
|
|
401
|
+
snapshot(): ScopeDiagnosticsSnapshot {
|
|
402
|
+
return buildSnapshot(this);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
listTools(): readonly EffectiveTool[] {
|
|
406
|
+
return collectEffectiveTools(this);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
listContextSections(): readonly EffectiveContextSection[] {
|
|
410
|
+
return collectEffectiveContextSections(this);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
async buildContext<TData = unknown>(data?: TData): Promise<readonly ProvidedContextSection[]> {
|
|
415
|
+
if (this.status !== 'active') {
|
|
416
|
+
throw new LifecycleError(`cannot build context: the ${this.kind} scope is ${this.status}`);
|
|
417
|
+
}
|
|
418
|
+
if (data !== undefined && (typeof data !== 'object' || data === null)) {
|
|
419
|
+
throw new MalformedPluginError('context assembly data must be an object when provided');
|
|
420
|
+
}
|
|
421
|
+
return buildContextSections(this, data);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Structural views used by diagnostics and validation.
|
|
425
|
+
|
|
426
|
+
get localServices(): ReadonlyMap<string, ResolvedService> {
|
|
427
|
+
const view = new Map<string, ResolvedService>();
|
|
428
|
+
for (const [keyId, registration] of this.services) {
|
|
429
|
+
view.set(keyId, {
|
|
430
|
+
key: registration.key,
|
|
431
|
+
providerPluginId: registration.providerPluginId,
|
|
432
|
+
providerScope: this.kind,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
return view;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
get localTools(): ReadonlyMap<string, LocalToolRegistration> {
|
|
439
|
+
return this.tools;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
get localContextSections(): ReadonlyMap<string, LocalContextSectionRegistration> {
|
|
443
|
+
return this.contextSections;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
get localListeners(): ReadonlyMap<string, LocalListenerRegistration> {
|
|
447
|
+
return this.listeners;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
get cleanupBarrierCount(): number {
|
|
452
|
+
return this.barriers.filter((barrier) => !barrier.removed).length;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
get childCount(): number {
|
|
456
|
+
return this.children.length;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
effectiveServiceInfos(): Map<string, ParentServiceInfo> {
|
|
460
|
+
const infos = new Map<string, ParentServiceInfo>();
|
|
461
|
+
this.collectServiceInfos(infos);
|
|
462
|
+
return infos;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private collectServiceInfos(infos: Map<string, ParentServiceInfo>): void {
|
|
466
|
+
for (const [keyId, registration] of this.services) {
|
|
467
|
+
if (infos.has(keyId)) continue;
|
|
468
|
+
infos.set(keyId, {
|
|
469
|
+
pluginId: registration.providerPluginId,
|
|
470
|
+
keyScope: registration.key.scope,
|
|
471
|
+
providerScope: this.kind,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
this.parent?.collectServiceInfos(infos);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
listenerChain(): {
|
|
478
|
+
kind: RuntimeScope;
|
|
479
|
+
listeners: readonly ListenerRegistration[];
|
|
480
|
+
}[] {
|
|
481
|
+
const chain: {
|
|
482
|
+
kind: RuntimeScope;
|
|
483
|
+
listeners: readonly ListenerRegistration[];
|
|
484
|
+
}[] = [];
|
|
485
|
+
this.collectListeners(chain);
|
|
486
|
+
return chain;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private collectListeners(chain: {
|
|
490
|
+
kind: RuntimeScope;
|
|
491
|
+
listeners: readonly ListenerRegistration[];
|
|
492
|
+
}[]): void {
|
|
493
|
+
chain.push({
|
|
494
|
+
kind: this.kind,
|
|
495
|
+
listeners: [...this.listeners.values()],
|
|
496
|
+
});
|
|
497
|
+
this.parent?.collectListeners(chain);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
addChild(child: ScopeBase): void {
|
|
501
|
+
this.children.push(child);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
removeChild(child: ScopeBase): void {
|
|
505
|
+
const index = this.children.indexOf(child);
|
|
506
|
+
if (index !== -1) this.children.splice(index, 1);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Disposal.
|
|
510
|
+
|
|
511
|
+
dispose(): Promise<void> {
|
|
512
|
+
if (this.status === 'disposed') return Promise.resolve();
|
|
513
|
+
if (this.disposePromise !== null) return this.disposePromise;
|
|
514
|
+
this.disposePromise = this.performDispose();
|
|
515
|
+
return this.disposePromise;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async disposeFromParent(): Promise<void> {
|
|
519
|
+
await this.dispose();
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
protected async performDispose(): Promise<void> {
|
|
523
|
+
this.status = 'disposing';
|
|
524
|
+
const failures: DisposalFailure[] = [];
|
|
525
|
+
|
|
526
|
+
// Unlink from the parent so a disposed scope is not retained. Parent
|
|
527
|
+
// disposal iterates a copy of its children, so this mutation is safe
|
|
528
|
+
// while the parent is disposing, and concurrent child disposal settles
|
|
529
|
+
// through the shared dispose promise.
|
|
530
|
+
this.parent?.removeChild(this);
|
|
531
|
+
|
|
532
|
+
for (const child of [...this.children].reverse()) {
|
|
533
|
+
try {
|
|
534
|
+
await child.disposeFromParent();
|
|
535
|
+
} catch (err) {
|
|
536
|
+
failures.push({ pluginId: `child:${child.scopeId}`, error: err });
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
for (const barrier of this.barriers) {
|
|
541
|
+
if (barrier.removed) continue;
|
|
542
|
+
try {
|
|
543
|
+
await barrier.fn();
|
|
544
|
+
} catch (err) {
|
|
545
|
+
failures.push({ pluginId: barrier.pluginId, error: err });
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const recordFailures = await disposeRecords(this.pluginRecords);
|
|
550
|
+
failures.push(...recordFailures);
|
|
551
|
+
|
|
552
|
+
this.status = 'disposed';
|
|
553
|
+
throwDisposalError(failures);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
export class ProcessScope extends ScopeBase {
|
|
558
|
+
readonly kind = 'process' as const;
|
|
559
|
+
readonly [processScopeBrand] = true as const;
|
|
560
|
+
|
|
561
|
+
private constructor() {
|
|
562
|
+
super(null, 'process');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
static async create(
|
|
566
|
+
plugins: readonly CapekPlugin<unknown>[],
|
|
567
|
+
options?: PluginOptionsMap,
|
|
568
|
+
): Promise<ProcessScope> {
|
|
569
|
+
const scope = new ProcessScope();
|
|
570
|
+
await activateScope(scope, plugins, options ?? {});
|
|
571
|
+
return scope;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
async createAgentScope(
|
|
575
|
+
plugins: readonly CapekPlugin<unknown>[],
|
|
576
|
+
options?: PluginOptionsMap,
|
|
577
|
+
): Promise<AgentScope> {
|
|
578
|
+
if (this.status !== 'active') {
|
|
579
|
+
throw new LifecycleError(
|
|
580
|
+
`cannot create an agent scope under a ${this.status} process scope`,
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
const agent = await AgentScope.create(this, plugins, options);
|
|
584
|
+
this.addChild(agent);
|
|
585
|
+
return agent;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export class AgentScope extends ScopeBase {
|
|
590
|
+
readonly kind = 'agent' as const;
|
|
591
|
+
readonly [agentScopeBrand] = true as const;
|
|
592
|
+
|
|
593
|
+
private constructor(parent: ProcessScope) {
|
|
594
|
+
super(parent, 'agent');
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
static async create(
|
|
598
|
+
parent: ProcessScope,
|
|
599
|
+
plugins: readonly CapekPlugin<unknown>[],
|
|
600
|
+
options?: PluginOptionsMap,
|
|
601
|
+
): Promise<AgentScope> {
|
|
602
|
+
const scope = new AgentScope(parent);
|
|
603
|
+
await activateScope(scope, plugins, options ?? {});
|
|
604
|
+
return scope;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
async createRunScope(
|
|
608
|
+
runId: string,
|
|
609
|
+
plugins: readonly CapekPlugin<unknown>[],
|
|
610
|
+
options?: PluginOptionsMap,
|
|
611
|
+
): Promise<RunScope> {
|
|
612
|
+
if (typeof runId !== 'string' || runId.length === 0) {
|
|
613
|
+
throw new MalformedPluginError('runId must be a non-empty string');
|
|
614
|
+
}
|
|
615
|
+
if (this.status !== 'active') {
|
|
616
|
+
throw new LifecycleError(
|
|
617
|
+
`cannot create a run scope under a ${this.status} agent scope`,
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
const run = await RunScope.create(this, runId, plugins, options);
|
|
621
|
+
this.addChild(run);
|
|
622
|
+
return run;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
export class RunScope extends ScopeBase {
|
|
627
|
+
readonly kind = 'run' as const;
|
|
628
|
+
readonly runId: string;
|
|
629
|
+
runStatus: RunStatus = 'created';
|
|
630
|
+
runOutcome: RunTerminalOutcome | undefined;
|
|
631
|
+
cancellationReason: string | undefined;
|
|
632
|
+
|
|
633
|
+
private terminalCompletion: Promise<void> | null = null;
|
|
634
|
+
private startDispatch: Promise<void> | null = null;
|
|
635
|
+
|
|
636
|
+
private constructor(parent: AgentScope, runId: string) {
|
|
637
|
+
super(parent, 'run');
|
|
638
|
+
this.runId = runId;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
static async create(
|
|
642
|
+
parent: AgentScope,
|
|
643
|
+
runId: string,
|
|
644
|
+
plugins: readonly CapekPlugin<unknown>[],
|
|
645
|
+
options?: PluginOptionsMap,
|
|
646
|
+
): Promise<RunScope> {
|
|
647
|
+
const scope = new RunScope(parent, runId);
|
|
648
|
+
await activateScope(scope, plugins, options ?? {});
|
|
649
|
+
return scope;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
async start(): Promise<void> {
|
|
653
|
+
if (this.runStatus !== 'created') {
|
|
654
|
+
throw new LifecycleError(
|
|
655
|
+
`run '${this.runId}' cannot start: already ${this.runStatus}`,
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
this.runStatus = 'running';
|
|
659
|
+
// Register the started dispatch before emit begins so a reentrant cancel
|
|
660
|
+
// from a synchronous started listener observes an in-flight startDispatch
|
|
661
|
+
// and serializes the terminal chain behind every started listener.
|
|
662
|
+
let resolveStarted!: () => void;
|
|
663
|
+
let rejectStarted!: (error: unknown) => void;
|
|
664
|
+
const started = new Promise<void>((resolve, reject) => {
|
|
665
|
+
resolveStarted = resolve;
|
|
666
|
+
rejectStarted = reject;
|
|
667
|
+
});
|
|
668
|
+
this.startDispatch = started;
|
|
669
|
+
this.emit({ type: 'run:started', runId: this.runId }).then(
|
|
670
|
+
() => {
|
|
671
|
+
if (this.startDispatch === started) this.startDispatch = null;
|
|
672
|
+
resolveStarted();
|
|
673
|
+
},
|
|
674
|
+
(error: unknown) => {
|
|
675
|
+
if (this.startDispatch === started) this.startDispatch = null;
|
|
676
|
+
rejectStarted(error);
|
|
677
|
+
},
|
|
678
|
+
);
|
|
679
|
+
await started;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
markTerminal(outcome: 'completed' | 'failed'): Promise<void> {
|
|
683
|
+
if (this.runStatus === 'terminal' || this.runStatus === 'disposed') {
|
|
684
|
+
return this.terminalCompletion ?? Promise.resolve();
|
|
685
|
+
}
|
|
686
|
+
this.runOutcome = outcome;
|
|
687
|
+
this.runStatus = 'terminal';
|
|
688
|
+
this.terminalCompletion = this.runTerminalChain();
|
|
689
|
+
return this.terminalCompletion;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
cancel(reason?: string): RunCancellation {
|
|
693
|
+
if (this.runStatus === 'terminal' || this.runStatus === 'disposed') {
|
|
694
|
+
return {
|
|
695
|
+
acknowledged: false,
|
|
696
|
+
completion: this.terminalCompletion ?? Promise.resolve(),
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
this.cancellationReason = reason;
|
|
700
|
+
this.runOutcome = 'cancelled';
|
|
701
|
+
this.runStatus = 'terminal';
|
|
702
|
+
this.terminalCompletion = this.runTerminalChain();
|
|
703
|
+
return { acknowledged: true, completion: this.terminalCompletion };
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
registerCleanupBarrier(barrier: CleanupBarrier): Disposable {
|
|
707
|
+
return this.registerBarrier(`run:${this.runId}`, barrier);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
override dispose(): Promise<void> {
|
|
711
|
+
if (this.status === 'disposed') return Promise.resolve();
|
|
712
|
+
if (this.runStatus === 'created' || this.runStatus === 'running') {
|
|
713
|
+
return Promise.reject(new LifecycleError(
|
|
714
|
+
`run '${this.runId}' has not reached terminal state; call cancel() or markTerminal() before disposal`,
|
|
715
|
+
));
|
|
716
|
+
}
|
|
717
|
+
return this.terminalCompletion ?? super.dispose();
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
override async disposeFromParent(): Promise<void> {
|
|
721
|
+
if (this.status === 'disposed') return;
|
|
722
|
+
if (this.runStatus === 'created' || this.runStatus === 'running') {
|
|
723
|
+
await this.cancel('parent scope disposed').completion;
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
if (this.terminalCompletion !== null) {
|
|
727
|
+
await this.terminalCompletion;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
private emit(event: KernelEvent): Promise<void> {
|
|
732
|
+
return dispatchEvent(this.listenerChain(), event);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
private async runTerminalChain(): Promise<void> {
|
|
736
|
+
const errors: unknown[] = [];
|
|
737
|
+
// Serialize terminal dispatch behind an in-flight started dispatch so
|
|
738
|
+
// events observe the run in order even when cancel races start.
|
|
739
|
+
const pendingStart = this.startDispatch;
|
|
740
|
+
if (pendingStart !== null) {
|
|
741
|
+
try {
|
|
742
|
+
await pendingStart;
|
|
743
|
+
} catch (err) {
|
|
744
|
+
errors.push(err);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
// Captured before disposal: plugin disposal removes listener
|
|
748
|
+
// registrations from the scope maps, but the run:disposed event must
|
|
749
|
+
// still reach every listener that observed this run.
|
|
750
|
+
const chain = this.listenerChain();
|
|
751
|
+
try {
|
|
752
|
+
await dispatchEvent(chain, {
|
|
753
|
+
type: 'run:terminal',
|
|
754
|
+
runId: this.runId,
|
|
755
|
+
outcome: this.runOutcome ?? 'cancelled',
|
|
756
|
+
reason: this.cancellationReason,
|
|
757
|
+
});
|
|
758
|
+
} catch (err) {
|
|
759
|
+
errors.push(err);
|
|
760
|
+
}
|
|
761
|
+
try {
|
|
762
|
+
await this.performDispose();
|
|
763
|
+
} catch (err) {
|
|
764
|
+
errors.push(err);
|
|
765
|
+
}
|
|
766
|
+
this.runStatus = 'disposed';
|
|
767
|
+
try {
|
|
768
|
+
await dispatchEvent(chain, { type: 'run:disposed', runId: this.runId });
|
|
769
|
+
} catch (err) {
|
|
770
|
+
errors.push(err);
|
|
771
|
+
}
|
|
772
|
+
if (errors.length > 0) {
|
|
773
|
+
throw new RunTerminalError(this.runId, errors);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|