@lensagent/core 0.1.0 → 0.1.2
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 +63 -4
- package/config/lens.example.jsonc +36 -13
- package/dist/adapters/executor-router.d.ts +20 -0
- package/dist/adapters/executor-router.js +86 -0
- package/dist/adapters/executor.d.ts +9 -3
- package/dist/adapters/executor.js +196 -61
- package/dist/adapters/lsp.d.ts +2 -1
- package/dist/adapters/lsp.js +98 -41
- package/dist/adapters/mcp.js +52 -17
- package/dist/adapters/revert.d.ts +5 -4
- package/dist/adapters/revert.js +6 -64
- package/dist/adapters/web.d.ts +9 -0
- package/dist/adapters/web.js +244 -0
- package/dist/config/files.d.ts +14 -0
- package/dist/config/files.js +57 -0
- package/dist/config/runtime.d.ts +3 -4
- package/dist/config/runtime.js +116 -97
- package/dist/config/schema.d.ts +56 -10
- package/dist/config/schema.js +48 -8
- package/dist/contracts/capability.d.ts +27 -1
- package/dist/contracts/domain.d.ts +1 -0
- package/dist/contracts/model.d.ts +33 -6
- package/dist/contracts/review.d.ts +17 -0
- package/dist/contracts/review.js +1 -0
- package/dist/core/access-policy.d.ts +40 -0
- package/dist/core/access-policy.js +68 -0
- package/dist/core/access.d.ts +30 -2
- package/dist/core/access.js +159 -26
- package/dist/core/context-boundaries.d.ts +25 -0
- package/dist/core/context-boundaries.js +106 -0
- package/dist/core/context.d.ts +3 -0
- package/dist/core/context.js +11 -0
- package/dist/core/host.js +261 -173
- package/dist/entry/pair.d.ts +1 -0
- package/dist/entry/pair.js +14 -0
- package/dist/entry/run.js +1 -1
- package/dist/execution/lsp-process.d.ts +3 -2
- package/dist/execution/lsp-process.js +46 -28
- package/dist/http/server.d.ts +5 -0
- package/dist/http/server.js +262 -80
- package/dist/runtime/agent.d.ts +9 -4
- package/dist/runtime/agent.js +361 -286
- package/dist/runtime/artifacts.d.ts +13 -1
- package/dist/runtime/artifacts.js +137 -10
- package/dist/runtime/cache.d.ts +59 -16
- package/dist/runtime/cache.js +124 -43
- package/dist/runtime/calls.d.ts +1 -1
- package/dist/runtime/calls.js +164 -48
- package/dist/runtime/capabilities.d.ts +2 -2
- package/dist/runtime/capabilities.js +4 -5
- package/dist/runtime/cleanup.d.ts +5 -2
- package/dist/runtime/cleanup.js +48 -17
- package/dist/runtime/compaction.d.ts +9 -0
- package/dist/runtime/compaction.js +52 -0
- package/dist/runtime/context.d.ts +8 -4
- package/dist/runtime/context.js +115 -128
- package/dist/runtime/events.d.ts +7 -0
- package/dist/runtime/events.js +83 -0
- package/dist/runtime/images.d.ts +14 -0
- package/dist/runtime/images.js +82 -0
- package/dist/runtime/kernel-capabilities.d.ts +7 -1
- package/dist/runtime/kernel-capabilities.js +146 -68
- package/dist/runtime/model-audit.d.ts +9 -0
- package/dist/runtime/model-audit.js +39 -0
- package/dist/runtime/model.d.ts +109 -4
- package/dist/runtime/model.js +368 -143
- package/dist/runtime/observability.d.ts +1 -0
- package/dist/runtime/observability.js +60 -15
- package/dist/runtime/policy.d.ts +1 -1
- package/dist/runtime/policy.js +2 -2
- package/dist/runtime/profile.d.ts +2 -1
- package/dist/runtime/profile.js +12 -15
- package/dist/runtime/projects.d.ts +3 -1
- package/dist/runtime/projects.js +38 -17
- package/dist/runtime/ptc-worker.d.ts +1 -0
- package/dist/runtime/ptc-worker.js +97 -0
- package/dist/runtime/ptc.d.ts +16 -0
- package/dist/runtime/ptc.js +128 -0
- package/dist/runtime/results.d.ts +5 -4
- package/dist/runtime/results.js +29 -51
- package/dist/runtime/review.d.ts +4 -1
- package/dist/runtime/review.js +101 -22
- package/dist/runtime/scheduler.js +1 -0
- package/dist/runtime/skills.js +34 -5
- package/dist/runtime/task-audit.d.ts +21 -0
- package/dist/runtime/task-audit.js +122 -0
- package/dist/runtime/time.d.ts +9 -0
- package/dist/runtime/time.js +18 -0
- package/dist/runtime/tokenizer.d.ts +2 -0
- package/dist/runtime/tokenizer.js +26 -1
- package/dist/store/database.js +26 -1
- package/dist/store/event-log.d.ts +1 -0
- package/dist/store/event-log.js +22 -5
- package/dist/store/schema.d.ts +1 -1
- package/dist/store/schema.js +50 -1
- package/dist/store/store.d.ts +65 -19
- package/dist/store/store.js +986 -188
- package/package.json +18 -8
- package/proto/executor.proto +69 -55
|
@@ -12,6 +12,11 @@ export class Observability {
|
|
|
12
12
|
#sequence = 0;
|
|
13
13
|
#writeTail = Promise.resolve();
|
|
14
14
|
#retentionChecked = false;
|
|
15
|
+
#queuedRecords = 0;
|
|
16
|
+
#queuedBytes = 0;
|
|
17
|
+
#lastWriteError = null;
|
|
18
|
+
#lastWarningAt = 0;
|
|
19
|
+
#closed = false;
|
|
15
20
|
constructor(root, config) {
|
|
16
21
|
this.#root = root;
|
|
17
22
|
this.#capacity = config?.traces?.capacity ?? 2048;
|
|
@@ -21,23 +26,45 @@ export class Observability {
|
|
|
21
26
|
const record = {
|
|
22
27
|
sequence: ++this.#sequence,
|
|
23
28
|
occurred_at: now(),
|
|
24
|
-
component,
|
|
25
|
-
operation,
|
|
26
|
-
outcome,
|
|
29
|
+
component: component.slice(0, 128),
|
|
30
|
+
operation: operation.slice(0, 256),
|
|
31
|
+
outcome: outcome.slice(0, 128),
|
|
27
32
|
duration_ms: durationMs,
|
|
28
|
-
fields
|
|
33
|
+
fields: safeFields(fields)
|
|
29
34
|
};
|
|
30
35
|
this.#push(this.#logs, record);
|
|
31
36
|
this.#push(this.#traces, record);
|
|
32
37
|
if (component === 'agent' || component === 'call')
|
|
33
38
|
this.#push(this.#agentOps, record);
|
|
34
39
|
this.increment(`${component}_${operation}_${outcome}`);
|
|
35
|
-
if (this.#logConfig?.enabled === true) {
|
|
36
|
-
|
|
40
|
+
if (this.#logConfig?.enabled === true && !this.#closed) {
|
|
41
|
+
const bytes = Buffer.byteLength(JSON.stringify(record));
|
|
42
|
+
if (this.#queuedRecords >= (this.#logConfig.maxQueuedRecords ?? 1024) || this.#queuedBytes + bytes > (this.#logConfig.maxQueuedBytes ?? 4194304)) {
|
|
43
|
+
this.increment('log_records_dropped');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
this.#queuedRecords++;
|
|
47
|
+
this.#queuedBytes += bytes;
|
|
48
|
+
this.#writeTail = this.#writeTail.then(() => this.#write(record)).catch((error) => {
|
|
49
|
+
this.#lastWriteError = typeof error === 'object' && error !== null && 'code' in error && typeof error.code === 'string' ? error.code.slice(0, 64) : 'LOG_WRITE_FAILED';
|
|
50
|
+
this.increment('log_write_failures');
|
|
51
|
+
if (Date.now() - this.#lastWarningAt > 60000) {
|
|
52
|
+
this.#lastWarningAt = Date.now();
|
|
53
|
+
process.stderr.write('Lens observability: log sink failed; see runtime diagnostics.\n');
|
|
54
|
+
}
|
|
55
|
+
}).finally(() => { this.#queuedRecords--; this.#queuedBytes -= bytes; });
|
|
37
56
|
}
|
|
38
57
|
}
|
|
39
58
|
increment(name, amount = 1) {
|
|
40
|
-
|
|
59
|
+
const key = metricName(name).slice(0, 128);
|
|
60
|
+
if (!this.#metrics.has(key) && this.#metrics.size >= 1024) {
|
|
61
|
+
this.#metrics.set('metric_series_dropped', (this.#metrics.get('metric_series_dropped') ?? 0) + 1);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.#metrics.set(key, (this.#metrics.get(key) ?? 0) + amount);
|
|
65
|
+
}
|
|
66
|
+
status() {
|
|
67
|
+
return { queued_records: this.#queuedRecords, queued_bytes: this.#queuedBytes, last_write_error: this.#lastWriteError, metric_series: this.#metrics.size };
|
|
41
68
|
}
|
|
42
69
|
logs(limit) {
|
|
43
70
|
return this.#logs.slice(-limit);
|
|
@@ -54,6 +81,7 @@ export class Observability {
|
|
|
54
81
|
.join('\n') + '\n';
|
|
55
82
|
}
|
|
56
83
|
async close() {
|
|
84
|
+
this.#closed = true;
|
|
57
85
|
await this.#writeTail;
|
|
58
86
|
}
|
|
59
87
|
#push(target, record) {
|
|
@@ -71,17 +99,18 @@ export class Observability {
|
|
|
71
99
|
this.#retentionChecked = true;
|
|
72
100
|
await pruneLogs(path, config.retentionDays);
|
|
73
101
|
}
|
|
74
|
-
const size = await stat(path).then((entry) => entry.size, () =>
|
|
102
|
+
const size = await stat(path).then((entry) => entry.size, (error) => { if (missing(error))
|
|
103
|
+
return 0; throw error; });
|
|
75
104
|
if (size >= config.maxFileBytes) {
|
|
76
105
|
if (config.maxFiles === 1)
|
|
77
|
-
await unlink(path)
|
|
106
|
+
await allowMissing(unlink(path));
|
|
78
107
|
else
|
|
79
|
-
await unlink(`${path}.${config.maxFiles - 1}`)
|
|
108
|
+
await allowMissing(unlink(`${path}.${config.maxFiles - 1}`));
|
|
80
109
|
for (let index = config.maxFiles - 2; index >= 1; index -= 1) {
|
|
81
|
-
await rename(`${path}.${index}`, `${path}.${index + 1}`)
|
|
110
|
+
await allowMissing(rename(`${path}.${index}`, `${path}.${index + 1}`));
|
|
82
111
|
}
|
|
83
112
|
if (config.maxFiles > 1)
|
|
84
|
-
await rename(path, `${path}.1`)
|
|
113
|
+
await allowMissing(rename(path, `${path}.1`));
|
|
85
114
|
}
|
|
86
115
|
await appendFile(path, `${JSON.stringify(record)}\n`, 'utf8');
|
|
87
116
|
}
|
|
@@ -90,15 +119,31 @@ async function pruneLogs(path, retentionDays) {
|
|
|
90
119
|
const threshold = Date.now() - retentionDays * 86400000;
|
|
91
120
|
const directory = dirname(path);
|
|
92
121
|
const name = basename(path);
|
|
93
|
-
for (const entry of await readdir(directory)
|
|
122
|
+
for (const entry of await readdir(directory)) {
|
|
94
123
|
if (entry !== name && !entry.startsWith(`${name}.`))
|
|
95
124
|
continue;
|
|
96
125
|
const candidate = resolve(directory, entry);
|
|
97
|
-
const modified = await stat(candidate).then((value) => value.mtimeMs, () =>
|
|
126
|
+
const modified = await stat(candidate).then((value) => value.mtimeMs, (error) => { if (missing(error))
|
|
127
|
+
return Date.now(); throw error; });
|
|
98
128
|
if (modified < threshold)
|
|
99
|
-
await unlink(candidate)
|
|
129
|
+
await allowMissing(unlink(candidate));
|
|
100
130
|
}
|
|
101
131
|
}
|
|
102
132
|
function metricName(value) {
|
|
103
133
|
return value.toLowerCase().replace(/[^a-z0-9_]/g, '_');
|
|
104
134
|
}
|
|
135
|
+
// Operational records contain identifiers and bounded status facts, never request bodies.
|
|
136
|
+
function safeFields(fields) {
|
|
137
|
+
const allowed = new Set(['project_id', 'requested_project_id', 'agent_id', 'call_id', 'run_id', 'step_id', 'state', 'outcome', 'outcome_unknown', 'error_code', 'error_name', 'status', 'reason', 'grace_ms', 'interrupted', 'server_id']);
|
|
138
|
+
return Object.fromEntries(Object.entries(fields).filter(([key, value]) => allowed.has(key) && (value === null || typeof value !== 'object')).map(([key, value]) => [key, typeof value === 'string' ? value.slice(0, 256) : value]));
|
|
139
|
+
}
|
|
140
|
+
function missing(error) { return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT'; }
|
|
141
|
+
async function allowMissing(operation) {
|
|
142
|
+
try {
|
|
143
|
+
await operation;
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (!missing(error))
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
}
|
package/dist/runtime/policy.d.ts
CHANGED
|
@@ -5,4 +5,4 @@ export interface PolicyDecision {
|
|
|
5
5
|
readonly autoReview: boolean;
|
|
6
6
|
}
|
|
7
7
|
export declare function decidePolicy(policy: Policy, effect: EffectScope): PolicyDecision;
|
|
8
|
-
export declare function policyPrompt(capabilityId: string): Json;
|
|
8
|
+
export declare function policyPrompt(capabilityId: string, reviewReason?: string): Json;
|
package/dist/runtime/policy.js
CHANGED
|
@@ -10,10 +10,10 @@ export function decidePolicy(policy, effect) {
|
|
|
10
10
|
return { state: 'QUEUED', autoReview: false };
|
|
11
11
|
fail('POLICY_DENIED', 403, `Policy ${policy} does not allow effect ${effect}.`);
|
|
12
12
|
}
|
|
13
|
-
export function policyPrompt(capabilityId) {
|
|
13
|
+
export function policyPrompt(capabilityId, reviewReason) {
|
|
14
14
|
return {
|
|
15
15
|
kind: 'policy_approval',
|
|
16
|
-
question: `Approve the Capability call ${capabilityId}
|
|
16
|
+
question: `Approve the Capability call ${capabilityId}?${reviewReason === undefined ? '' : `\nAuto review: ${reviewReason}`}`,
|
|
17
17
|
options: [{ label: 'Approve', value: 'approve' }, { label: 'Reject', value: 'reject' }],
|
|
18
18
|
allow_freeform: false,
|
|
19
19
|
multi_select: false
|
|
@@ -5,4 +5,5 @@ export interface ResolvedProfile {
|
|
|
5
5
|
readonly value: ModelHarnessProfile;
|
|
6
6
|
}
|
|
7
7
|
export declare function resolveProfile(config: HarnessConfig, id?: string): ResolvedProfile;
|
|
8
|
-
export declare function
|
|
8
|
+
export declare function modelReasoningEfforts(config: HarnessConfig, reference: string): readonly string[];
|
|
9
|
+
export declare function resolveReasoningEffort(config: HarnessConfig, reference: string, configured: string | null): string;
|
package/dist/runtime/profile.js
CHANGED
|
@@ -20,13 +20,6 @@ const genericProfile = {
|
|
|
20
20
|
toolDescriptionStyle: 'plain',
|
|
21
21
|
recoveryConstraints: { partialResponseResume: 'forbidden', preserveReasoningState: false }
|
|
22
22
|
},
|
|
23
|
-
ptc: {
|
|
24
|
-
enabledByDefault: false,
|
|
25
|
-
maxToolCalls: 32,
|
|
26
|
-
schemaVersion: 'programmatic-tool-run-v1',
|
|
27
|
-
instruction: 'Run bounded tool-heavy work through the hosted programmatic tool runtime. Use only the capability IDs frozen in this request.'
|
|
28
|
-
},
|
|
29
|
-
effort: { allowed: ['none', 'minimal', 'low', 'medium', 'high'], default: 'medium' },
|
|
30
23
|
cache: {
|
|
31
24
|
enabledByDefault: false,
|
|
32
25
|
minimumEligiblePrefixTokens: 1024,
|
|
@@ -38,15 +31,19 @@ export function resolveProfile(config, id = 'generic-v1') {
|
|
|
38
31
|
const value = id === 'generic-v1' ? genericProfile : config.modelHarnessProfiles?.[id];
|
|
39
32
|
if (value === undefined)
|
|
40
33
|
fail('MODEL_PROFILE_NOT_FOUND', 503, `Model Harness Profile ${id} is not configured.`);
|
|
41
|
-
if (!value.effort.allowed.includes(value.effort.default)) {
|
|
42
|
-
fail('MODEL_PROFILE_INVALID', 503, `Model Harness Profile ${id} has an invalid default reasoning effort.`);
|
|
43
|
-
}
|
|
44
34
|
return { id, digest: digest(value), value };
|
|
45
35
|
}
|
|
46
|
-
export function
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
36
|
+
export function modelReasoningEfforts(config, reference) {
|
|
37
|
+
const slash = reference.indexOf('/');
|
|
38
|
+
const provider = config.providers[reference.slice(0, slash)];
|
|
39
|
+
const model = provider?.models[reference.slice(slash + 1)];
|
|
40
|
+
if (provider === undefined || model === undefined)
|
|
41
|
+
fail('MODEL_NOT_FOUND', 404, `Model ${reference} is not configured.`);
|
|
42
|
+
return model.reasoningEfforts ?? provider.reasoningEfforts ?? [];
|
|
43
|
+
}
|
|
44
|
+
export function resolveReasoningEffort(config, reference, configured) {
|
|
45
|
+
if (configured === null || !modelReasoningEfforts(config, reference).includes(configured)) {
|
|
46
|
+
fail('MODEL_EFFORT_UNSUPPORTED', 400, `Reasoning effort ${configured ?? '(not selected)'} is not in the configured reasoningEfforts for ${reference}.`);
|
|
50
47
|
}
|
|
51
|
-
return
|
|
48
|
+
return configured;
|
|
52
49
|
}
|
|
@@ -18,6 +18,7 @@ export interface ProjectRuntime {
|
|
|
18
18
|
readonly context: ContextEngine;
|
|
19
19
|
readonly agents: AgentRuntime;
|
|
20
20
|
contractDigest(): string;
|
|
21
|
+
capabilitiesDigest(): string;
|
|
21
22
|
refreshSkills(): void;
|
|
22
23
|
}
|
|
23
24
|
export declare class ProjectRuntimes {
|
|
@@ -32,7 +33,8 @@ export declare class ProjectRuntimes {
|
|
|
32
33
|
readonly target: AdapterTarget;
|
|
33
34
|
readonly events: EventBus;
|
|
34
35
|
readonly pinnedProjectId: string;
|
|
35
|
-
|
|
36
|
+
readonly sharedWorkspace: (path: string, executorId?: string) => boolean;
|
|
37
|
+
constructor(store: Store, config: ConfigRuntime, scheduler: Scheduler, artifacts: ArtifactStore, models: AiModelGateway, reviewer: AutoReviewer, sharedCapabilities: readonly RegisteredCapability[], target: AdapterTarget, events: EventBus, pinnedProjectId: string, sharedWorkspace?: (path: string, executorId?: string) => boolean);
|
|
36
38
|
get(projectId: string): ProjectRuntime;
|
|
37
39
|
refresh(projectId: string): void;
|
|
38
40
|
startIdleEviction(maxIdleMs?: number): void;
|
package/dist/runtime/projects.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { digest } from '../core/codec.js';
|
|
1
2
|
import { CapabilityRegistry } from './capabilities.js';
|
|
2
3
|
import { CallRuntime } from './calls.js';
|
|
3
4
|
import { ContextEngine } from './context.js';
|
|
4
5
|
import { AgentRuntime } from './agent.js';
|
|
5
6
|
import { KernelAdapter, kernelCapabilities } from './kernel-capabilities.js';
|
|
6
7
|
import { workspaceSkills } from './skills.js';
|
|
8
|
+
import { webCapabilities } from '../adapters/web.js';
|
|
7
9
|
import { coreManifests, manifestSetDigest } from './manifests.js';
|
|
8
10
|
export class ProjectRuntimes {
|
|
9
11
|
store;
|
|
@@ -16,10 +18,11 @@ export class ProjectRuntimes {
|
|
|
16
18
|
target;
|
|
17
19
|
events;
|
|
18
20
|
pinnedProjectId;
|
|
21
|
+
sharedWorkspace;
|
|
19
22
|
#runtimes = new Map();
|
|
20
23
|
#evictionTimer;
|
|
21
24
|
#closed = false;
|
|
22
|
-
constructor(store, config, scheduler, artifacts, models, reviewer, sharedCapabilities, target, events, pinnedProjectId) {
|
|
25
|
+
constructor(store, config, scheduler, artifacts, models, reviewer, sharedCapabilities, target, events, pinnedProjectId, sharedWorkspace = () => false) {
|
|
23
26
|
this.store = store;
|
|
24
27
|
this.config = config;
|
|
25
28
|
this.scheduler = scheduler;
|
|
@@ -30,6 +33,7 @@ export class ProjectRuntimes {
|
|
|
30
33
|
this.target = target;
|
|
31
34
|
this.events = events;
|
|
32
35
|
this.pinnedProjectId = pinnedProjectId;
|
|
36
|
+
this.sharedWorkspace = sharedWorkspace;
|
|
33
37
|
}
|
|
34
38
|
get(projectId) {
|
|
35
39
|
if (this.#closed)
|
|
@@ -60,7 +64,10 @@ export class ProjectRuntimes {
|
|
|
60
64
|
this.#evictionTimer = undefined;
|
|
61
65
|
const runtimes = [...this.#runtimes.values()].map((entry) => entry.runtime);
|
|
62
66
|
this.#runtimes.clear();
|
|
63
|
-
await Promise.allSettled(runtimes.flatMap((runtime) => [runtime.agents.close(), runtime.calls.close()]));
|
|
67
|
+
const results = await Promise.allSettled(runtimes.flatMap((runtime) => [runtime.agents.close(), runtime.calls.close(runtime.projectId)]));
|
|
68
|
+
const failures = results.filter((result) => result.status === 'rejected');
|
|
69
|
+
if (failures.length > 0)
|
|
70
|
+
throw new AggregateError(failures.map((result) => result.reason), 'Project runtime cleanup failed.');
|
|
64
71
|
}
|
|
65
72
|
async #evictIdle(maxIdleMs) {
|
|
66
73
|
const timestamp = Date.now();
|
|
@@ -71,34 +78,50 @@ export class ProjectRuntimes {
|
|
|
71
78
|
entry.lastAccessedAt = timestamp;
|
|
72
79
|
continue;
|
|
73
80
|
}
|
|
74
|
-
|
|
75
|
-
await Promise.allSettled([entry.runtime.agents.close(), entry.runtime.calls.close()]);
|
|
76
|
-
if (entry.lastAccessedAt === lastAccessedAt && !this.store.hasActiveRuntimeObjects(projectId))
|
|
77
|
-
this.#runtimes.delete(projectId);
|
|
81
|
+
this.#runtimes.delete(projectId);
|
|
82
|
+
await Promise.allSettled([entry.runtime.agents.close(), entry.runtime.calls.close(entry.runtime.projectId)]);
|
|
78
83
|
}
|
|
79
84
|
}
|
|
80
85
|
#build(projectId) {
|
|
81
86
|
const registry = new CapabilityRegistry(this.target);
|
|
82
87
|
for (const capability of this.sharedCapabilities)
|
|
83
88
|
registry.register(capability);
|
|
89
|
+
for (const capability of webCapabilities())
|
|
90
|
+
registry.register(capability);
|
|
84
91
|
const context = new ContextEngine(this.store, registry);
|
|
85
92
|
const calls = new CallRuntime(this.store, registry, this.scheduler, this.artifacts, this.events);
|
|
86
|
-
const agents = new AgentRuntime(this.store, this.config, context, this.models, registry, calls, this.reviewer, this.events);
|
|
87
|
-
const kernel = new KernelAdapter(this.store, registry, this.config, (targetProjectId, agentId) => this.get(targetProjectId).agents.wake(targetProjectId, agentId), (targetProjectId, input, signal) => this.get(targetProjectId).agents.createSubagent(targetProjectId, input, signal));
|
|
93
|
+
const agents = new AgentRuntime(this.store, this.config, context, this.models, registry, calls, this.reviewer, this.events, () => refreshSkills());
|
|
94
|
+
const kernel = new KernelAdapter(this.store, registry, this.config, (targetProjectId, agentId) => this.get(targetProjectId).agents.wake(targetProjectId, agentId), (targetProjectId, input, signal) => this.get(targetProjectId).agents.createSubagent(targetProjectId, input, signal), (targetProjectId, agentId, focus, signal) => this.get(targetProjectId).agents.compactFromTool(targetProjectId, agentId, focus, signal), (invocation) => agents.executeProgram(invocation), this.artifacts);
|
|
88
95
|
for (const capability of kernelCapabilities(kernel))
|
|
89
96
|
registry.register(capability);
|
|
97
|
+
const definitionsDigest = () => digest(JSON.parse(JSON.stringify(registry.list())));
|
|
90
98
|
const refreshSkills = () => {
|
|
91
|
-
|
|
99
|
+
const previous = definitionsDigest();
|
|
92
100
|
const paths = this.store.listWorkspaces(projectId).flatMap((workspace) => {
|
|
93
101
|
const value = workspace.paths;
|
|
94
|
-
return Array.isArray(value) ? value.filter((entry) => typeof entry === 'string') : [];
|
|
102
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === 'string' && this.sharedWorkspace(entry, typeof workspace.executor_id === 'string' ? workspace.executor_id : 'remote')) : [];
|
|
95
103
|
});
|
|
96
|
-
|
|
104
|
+
let skills;
|
|
105
|
+
try {
|
|
106
|
+
skills = workspaceSkills(paths);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Keep the last complete catalog when a Workspace temporarily becomes inaccessible.
|
|
110
|
+
this.events.emit(projectId, this.store.stateRevision(), 'runtime.notice', { code: 'skill_refresh_failed', component: 'capabilities', message: 'Workspace Skills could not be refreshed; the previous catalog remains active.' });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
registry.removeKind('skill');
|
|
114
|
+
for (const capability of skills)
|
|
97
115
|
registry.register(capability);
|
|
116
|
+
if (definitionsDigest() !== previous) {
|
|
117
|
+
this.events.emit(projectId, this.store.stateRevision(), 'runtime.notice', { code: 'snapshot_required', component: 'capabilities', project_capabilities_digest: definitionsDigest() });
|
|
118
|
+
}
|
|
98
119
|
};
|
|
120
|
+
const contractDigest = () => digest({
|
|
121
|
+
manifests: manifestSetDigest([...coreManifests(this.config.current.config), ...registry.contractManifests()], this.target),
|
|
122
|
+
capabilities: JSON.parse(JSON.stringify(registry.list().filter(definition => definition.kind !== 'skill')))
|
|
123
|
+
});
|
|
99
124
|
refreshSkills();
|
|
100
|
-
for (const callId of this.store.queuedDirectCallIds(projectId))
|
|
101
|
-
calls.dispatch(projectId, callId);
|
|
102
125
|
return {
|
|
103
126
|
projectId,
|
|
104
127
|
registry,
|
|
@@ -106,10 +129,8 @@ export class ProjectRuntimes {
|
|
|
106
129
|
context,
|
|
107
130
|
agents,
|
|
108
131
|
refreshSkills,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
...registry.contractManifests()
|
|
112
|
-
], this.target)
|
|
132
|
+
capabilitiesDigest: definitionsDigest,
|
|
133
|
+
contractDigest
|
|
113
134
|
};
|
|
114
135
|
}
|
|
115
136
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
2
|
+
import { stripTypeScriptTypes } from 'node:module';
|
|
3
|
+
import { getQuickJS } from 'quickjs-emscripten';
|
|
4
|
+
const port = parentPort;
|
|
5
|
+
if (port === null)
|
|
6
|
+
throw new Error('PTC must run in a worker.');
|
|
7
|
+
const input = workerData;
|
|
8
|
+
const module = await getQuickJS();
|
|
9
|
+
const runtime = module.newRuntime();
|
|
10
|
+
runtime.setMemoryLimit(64 * 1024 * 1024);
|
|
11
|
+
runtime.setMaxStackSize(1024 * 1024);
|
|
12
|
+
let deadline = Date.now() + input.timeoutMs;
|
|
13
|
+
runtime.setInterruptHandler(() => Date.now() >= deadline);
|
|
14
|
+
const vm = runtime.newContext();
|
|
15
|
+
const pending = new Map();
|
|
16
|
+
let sequence = 0;
|
|
17
|
+
let outputBytes = 0;
|
|
18
|
+
const logs = [];
|
|
19
|
+
let finished = false;
|
|
20
|
+
const isFinished = () => finished;
|
|
21
|
+
function report(error) {
|
|
22
|
+
if (isFinished())
|
|
23
|
+
return;
|
|
24
|
+
finished = true;
|
|
25
|
+
port?.postMessage({ type: 'error', error: error instanceof Error ? error.message : String(error) });
|
|
26
|
+
}
|
|
27
|
+
function pump() {
|
|
28
|
+
const result = runtime.executePendingJobs();
|
|
29
|
+
if (result.error !== undefined) {
|
|
30
|
+
const error = vm.dump(result.error);
|
|
31
|
+
result.error.dispose();
|
|
32
|
+
report(JSON.stringify(error));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const call = vm.newFunction('__call', (name, args) => {
|
|
37
|
+
const deferred = vm.newPromise();
|
|
38
|
+
const id = sequence++;
|
|
39
|
+
pending.set(id, deferred);
|
|
40
|
+
port.postMessage({ type: 'call', id, name: vm.getString(name), arguments: vm.getString(args) });
|
|
41
|
+
return deferred.handle;
|
|
42
|
+
});
|
|
43
|
+
vm.setProp(vm.global, '__call', call);
|
|
44
|
+
call.dispose();
|
|
45
|
+
const log = vm.newFunction('__log', (value) => {
|
|
46
|
+
const text = vm.getString(value);
|
|
47
|
+
outputBytes += Buffer.byteLength(text);
|
|
48
|
+
if (outputBytes > 1024 * 1024)
|
|
49
|
+
throw new Error('PTC output exceeds 1 MiB; return selected data.');
|
|
50
|
+
logs.push(text);
|
|
51
|
+
});
|
|
52
|
+
vm.setProp(vm.global, '__log', log);
|
|
53
|
+
log.dispose();
|
|
54
|
+
const bootstrap = vm.evalCode(`
|
|
55
|
+
const tools = Object.freeze(Object.fromEntries(${JSON.stringify(input.capabilityIds)}.map(name => [name, async (args = {}) => JSON.parse(await __call(name, JSON.stringify(args)))])));
|
|
56
|
+
const console = Object.freeze({ log: (...args) => __log(args.map(x => typeof x === 'string' ? x : JSON.stringify(x)).join(' ')) });
|
|
57
|
+
`);
|
|
58
|
+
vm.unwrapResult(bootstrap).dispose();
|
|
59
|
+
port.on('message', (message) => {
|
|
60
|
+
if (isFinished())
|
|
61
|
+
return;
|
|
62
|
+
const deferred = pending.get(message.id);
|
|
63
|
+
if (deferred === undefined)
|
|
64
|
+
return;
|
|
65
|
+
pending.delete(message.id);
|
|
66
|
+
const value = message.error === undefined ? vm.newString(message.value ?? 'null') : vm.newError(message.error);
|
|
67
|
+
if (message.error === undefined)
|
|
68
|
+
deferred.resolve(value);
|
|
69
|
+
else
|
|
70
|
+
deferred.reject(value);
|
|
71
|
+
value.dispose();
|
|
72
|
+
deferred.dispose();
|
|
73
|
+
deadline = Date.now() + input.timeoutMs;
|
|
74
|
+
pump();
|
|
75
|
+
});
|
|
76
|
+
// Strip an async function body so top-level await/return and erasable TS annotations work.
|
|
77
|
+
const wrapped = `(async () => {\n${input.program}\n})()`;
|
|
78
|
+
const code = input.language === 'typescript' ? stripTypeScriptTypes(wrapped, { mode: 'transform' }) : wrapped;
|
|
79
|
+
const result = vm.unwrapResult(vm.evalCode(code, 'program.js'));
|
|
80
|
+
const settled = vm.resolvePromise(result);
|
|
81
|
+
pump();
|
|
82
|
+
const value = vm.unwrapResult(await settled);
|
|
83
|
+
if (!isFinished()) {
|
|
84
|
+
const content = vm.dump(value);
|
|
85
|
+
const serialized = JSON.stringify({ value: content ?? null, logs });
|
|
86
|
+
if (Buffer.byteLength(serialized) > 1024 * 1024)
|
|
87
|
+
throw new Error('PTC output exceeds 1 MiB; return selected data.');
|
|
88
|
+
finished = true;
|
|
89
|
+
port.postMessage({ type: 'done', value: serialized });
|
|
90
|
+
}
|
|
91
|
+
value.dispose();
|
|
92
|
+
result.dispose();
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
report(error);
|
|
96
|
+
}
|
|
97
|
+
// Parent terminates the worker on every exit path, including pending promises and runaway jobs.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Json } from '../core/domain.js';
|
|
2
|
+
export interface LocalProgram {
|
|
3
|
+
readonly program: string;
|
|
4
|
+
readonly language: 'javascript' | 'typescript';
|
|
5
|
+
readonly capabilityIds: readonly string[];
|
|
6
|
+
readonly timeoutMs: number;
|
|
7
|
+
readonly maxToolCalls: number;
|
|
8
|
+
readonly signal: AbortSignal;
|
|
9
|
+
readonly waiting?: {
|
|
10
|
+
read(): boolean;
|
|
11
|
+
subscribe(changed: () => void): () => void;
|
|
12
|
+
};
|
|
13
|
+
execute(capabilityId: string, args: Json, ordinal: number, signal: AbortSignal): Promise<Json>;
|
|
14
|
+
}
|
|
15
|
+
/** A fresh isolated interpreter per call. Only JSON crosses its tool bridge. */
|
|
16
|
+
export declare function runLocalProgram(input: LocalProgram): Promise<Json>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Worker } from 'node:worker_threads';
|
|
2
|
+
import { fail } from '../core/domain.js';
|
|
3
|
+
/** A fresh isolated interpreter per call. Only JSON crosses its tool bridge. */
|
|
4
|
+
export async function runLocalProgram(input) {
|
|
5
|
+
input.signal.throwIfAborted();
|
|
6
|
+
if (Buffer.byteLength(input.program) > 262144)
|
|
7
|
+
fail('PTC_INPUT_LIMIT', 400, 'Program exceeds 256 KiB.');
|
|
8
|
+
const controller = new AbortController();
|
|
9
|
+
const abort = () => controller.abort(input.signal.reason);
|
|
10
|
+
input.signal.addEventListener('abort', abort, { once: true });
|
|
11
|
+
const worker = new Worker(new URL(import.meta.url.endsWith('.ts') ? './ptc-worker.ts' : './ptc-worker.js', import.meta.url), {
|
|
12
|
+
workerData: { program: input.program, language: input.language, capabilityIds: input.capabilityIds, timeoutMs: input.timeoutMs },
|
|
13
|
+
execArgv: [], env: {}, resourceLimits: { maxOldGenerationSizeMb: 128, stackSizeMb: 4 }
|
|
14
|
+
});
|
|
15
|
+
const pending = new Set();
|
|
16
|
+
let count = 0;
|
|
17
|
+
let bridgeBytes = 0;
|
|
18
|
+
let stopped = false;
|
|
19
|
+
const isStopped = () => stopped;
|
|
20
|
+
let timer;
|
|
21
|
+
let unsubscribe;
|
|
22
|
+
try {
|
|
23
|
+
return await new Promise((resolve, reject) => {
|
|
24
|
+
const stop = (error) => {
|
|
25
|
+
if (isStopped())
|
|
26
|
+
return;
|
|
27
|
+
stopped = true;
|
|
28
|
+
controller.abort(error);
|
|
29
|
+
reject(error);
|
|
30
|
+
};
|
|
31
|
+
const failure = (code, message) => stop(Object.assign(new Error(message), { code }));
|
|
32
|
+
controller.signal.addEventListener('abort', () => stop(controller.signal.reason instanceof Error ? controller.signal.reason : new Error('Program cancelled.')), { once: true });
|
|
33
|
+
let remaining = input.timeoutMs;
|
|
34
|
+
let started = performance.now();
|
|
35
|
+
let paused = false;
|
|
36
|
+
const refreshDeadline = () => {
|
|
37
|
+
if (isStopped())
|
|
38
|
+
return;
|
|
39
|
+
const now = performance.now();
|
|
40
|
+
if (!paused)
|
|
41
|
+
remaining -= now - started;
|
|
42
|
+
started = now;
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
try {
|
|
45
|
+
paused = input.waiting?.read() === true;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
stop(error instanceof Error ? error : new Error(String(error)));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// Policy review and user input pause the remaining execution budget.
|
|
52
|
+
if (!paused)
|
|
53
|
+
timer = setTimeout(() => failure('PTC_TIMEOUT', 'Local program exceeded its deadline.'), Math.max(0, remaining));
|
|
54
|
+
};
|
|
55
|
+
unsubscribe = input.waiting?.subscribe(refreshDeadline);
|
|
56
|
+
refreshDeadline();
|
|
57
|
+
worker.on('error', (error) => stop(error));
|
|
58
|
+
worker.on('exit', (code) => { if (!isStopped())
|
|
59
|
+
failure('PTC_WORKER_EXIT', `Local program worker exited without a result (${code}).`); });
|
|
60
|
+
worker.on('message', (message) => {
|
|
61
|
+
if (isStopped())
|
|
62
|
+
return;
|
|
63
|
+
if (message.type === 'error') {
|
|
64
|
+
failure('PTC_EXECUTION_FAILED', message.error ?? 'Local program failed.');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (message.type === 'done') {
|
|
68
|
+
if (pending.size > 0) {
|
|
69
|
+
failure('PTC_UNAWAITED_TOOLS', 'Await all tool calls before returning.');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
stopped = true;
|
|
73
|
+
resolve(JSON.parse(message.value ?? 'null'));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (message.type !== 'call' || typeof message.name !== 'string' || typeof message.arguments !== 'string' || typeof message.id !== 'number') {
|
|
77
|
+
failure('PTC_BRIDGE_INVALID', 'Invalid tool bridge request.');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (!input.capabilityIds.includes(message.name) || message.name === 'programmatic_tool_run') {
|
|
81
|
+
failure('CAPABILITY_NOT_LOADED', 'Tool is not in this program capability set.');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (++count > input.maxToolCalls) {
|
|
85
|
+
failure('PTC_CALL_LIMIT', 'Local program exceeded its tool call limit.');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const id = message.id;
|
|
89
|
+
const name = message.name;
|
|
90
|
+
const argumentsText = message.arguments;
|
|
91
|
+
bridgeBytes += Buffer.byteLength(argumentsText);
|
|
92
|
+
if (bridgeBytes > 32 * 1024 * 1024) {
|
|
93
|
+
failure('PTC_BRIDGE_LIMIT', 'Program tool traffic exceeds 32 MiB; narrow tool queries.');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const execution = (async () => {
|
|
97
|
+
try {
|
|
98
|
+
const output = await input.execute(name, JSON.parse(argumentsText), count - 1, controller.signal);
|
|
99
|
+
if (isStopped())
|
|
100
|
+
return;
|
|
101
|
+
const value = JSON.stringify(output);
|
|
102
|
+
bridgeBytes += Buffer.byteLength(value);
|
|
103
|
+
if (bridgeBytes > 32 * 1024 * 1024) {
|
|
104
|
+
failure('PTC_BRIDGE_LIMIT', 'Program tool traffic exceeds 32 MiB; narrow tool queries.');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
worker.postMessage({ id, value });
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (!isStopped())
|
|
111
|
+
worker.postMessage({ id, error: error instanceof Error ? error.message : String(error) });
|
|
112
|
+
}
|
|
113
|
+
})();
|
|
114
|
+
pending.add(execution);
|
|
115
|
+
void execution.finally(() => pending.delete(execution));
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
stopped = true;
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
unsubscribe?.();
|
|
123
|
+
input.signal.removeEventListener('abort', abort);
|
|
124
|
+
controller.abort(new Error('Local program finished.'));
|
|
125
|
+
await worker.terminate();
|
|
126
|
+
await Promise.allSettled(pending);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import type { CapabilityOutput, Json } from '../core/domain.js';
|
|
2
|
+
import type { ArtifactStore } from './artifacts.js';
|
|
3
|
+
export interface StoredOutput {
|
|
3
4
|
readonly content: Json;
|
|
4
|
-
readonly artifacts: readonly
|
|
5
|
+
readonly artifacts: readonly string[];
|
|
5
6
|
readonly truncated: boolean;
|
|
6
7
|
}
|
|
7
|
-
export declare function
|
|
8
|
+
export declare function storeOutput(projectId: string, output: CapabilityOutput, store: ArtifactStore, preview?: boolean): Promise<StoredOutput>;
|