@lensagent/core 0.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/LICENSE +21 -0
- package/README.md +48 -0
- package/config/lens.example.jsonc +92 -0
- package/config/pair.example.json +3 -0
- package/dist/adapters/executor.d.ts +35 -0
- package/dist/adapters/executor.js +296 -0
- package/dist/adapters/lsp.d.ts +18 -0
- package/dist/adapters/lsp.js +218 -0
- package/dist/adapters/mcp.d.ts +32 -0
- package/dist/adapters/mcp.js +283 -0
- package/dist/adapters/revert.d.ts +12 -0
- package/dist/adapters/revert.js +155 -0
- package/dist/config/runtime.d.ts +55 -0
- package/dist/config/runtime.js +371 -0
- package/dist/config/schema.d.ts +292 -0
- package/dist/config/schema.js +177 -0
- package/dist/contracts/adapter.d.ts +28 -0
- package/dist/contracts/adapter.js +8 -0
- package/dist/contracts/capability.d.ts +41 -0
- package/dist/contracts/capability.js +1 -0
- package/dist/contracts/domain.d.ts +61 -0
- package/dist/contracts/domain.js +1 -0
- package/dist/contracts/index.d.ts +5 -0
- package/dist/contracts/index.js +5 -0
- package/dist/contracts/model.d.ts +77 -0
- package/dist/contracts/model.js +1 -0
- package/dist/contracts/ports.d.ts +14 -0
- package/dist/contracts/ports.js +14 -0
- package/dist/core/access.d.ts +24 -0
- package/dist/core/access.js +205 -0
- package/dist/core/codec.d.ts +33 -0
- package/dist/core/codec.js +105 -0
- package/dist/core/domain.d.ts +13 -0
- package/dist/core/domain.js +24 -0
- package/dist/core/host.d.ts +19 -0
- package/dist/core/host.js +249 -0
- package/dist/core/identity.d.ts +19 -0
- package/dist/core/identity.js +54 -0
- package/dist/entry/bun.d.ts +1 -0
- package/dist/entry/bun.js +2 -0
- package/dist/entry/node.d.ts +2 -0
- package/dist/entry/node.js +3 -0
- package/dist/entry/rebuild.d.ts +1 -0
- package/dist/entry/rebuild.js +47 -0
- package/dist/entry/run.d.ts +1 -0
- package/dist/entry/run.js +43 -0
- package/dist/execution/lsp-process.d.ts +14 -0
- package/dist/execution/lsp-process.js +234 -0
- package/dist/http/server.d.ts +34 -0
- package/dist/http/server.js +1332 -0
- package/dist/launch.d.ts +3 -0
- package/dist/launch.js +4 -0
- package/dist/runtime/agent.d.ts +35 -0
- package/dist/runtime/agent.js +853 -0
- package/dist/runtime/artifacts.d.ts +15 -0
- package/dist/runtime/artifacts.js +72 -0
- package/dist/runtime/cache.d.ts +20 -0
- package/dist/runtime/cache.js +44 -0
- package/dist/runtime/calls.d.ts +21 -0
- package/dist/runtime/calls.js +130 -0
- package/dist/runtime/capabilities.d.ts +58 -0
- package/dist/runtime/capabilities.js +196 -0
- package/dist/runtime/cleanup.d.ts +7 -0
- package/dist/runtime/cleanup.js +33 -0
- package/dist/runtime/context.d.ts +34 -0
- package/dist/runtime/context.js +169 -0
- package/dist/runtime/diagnostics.d.ts +8 -0
- package/dist/runtime/diagnostics.js +71 -0
- package/dist/runtime/events.d.ts +22 -0
- package/dist/runtime/events.js +131 -0
- package/dist/runtime/kernel-capabilities.d.ts +32 -0
- package/dist/runtime/kernel-capabilities.js +200 -0
- package/dist/runtime/manifests.d.ts +14 -0
- package/dist/runtime/manifests.js +117 -0
- package/dist/runtime/model.d.ts +20 -0
- package/dist/runtime/model.js +241 -0
- package/dist/runtime/observability.d.ts +22 -0
- package/dist/runtime/observability.js +104 -0
- package/dist/runtime/policy.d.ts +8 -0
- package/dist/runtime/policy.js +31 -0
- package/dist/runtime/profile.d.ts +8 -0
- package/dist/runtime/profile.js +52 -0
- package/dist/runtime/projects.d.ts +40 -0
- package/dist/runtime/projects.js +115 -0
- package/dist/runtime/results.d.ts +7 -0
- package/dist/runtime/results.js +55 -0
- package/dist/runtime/review.d.ts +9 -0
- package/dist/runtime/review.js +54 -0
- package/dist/runtime/scheduler.d.ts +15 -0
- package/dist/runtime/scheduler.js +85 -0
- package/dist/runtime/skills.d.ts +2 -0
- package/dist/runtime/skills.js +83 -0
- package/dist/runtime/tokenizer.d.ts +8 -0
- package/dist/runtime/tokenizer.js +30 -0
- package/dist/store/database.d.ts +3 -0
- package/dist/store/database.js +49 -0
- package/dist/store/event-log.d.ts +19 -0
- package/dist/store/event-log.js +61 -0
- package/dist/store/schema.d.ts +1 -0
- package/dist/store/schema.js +347 -0
- package/dist/store/store.d.ts +344 -0
- package/dist/store/store.js +2554 -0
- package/package.json +73 -0
- package/proto/executor.proto +314 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { digest } from '../core/codec.js';
|
|
2
|
+
import { ADAPTER_CONTRACT_VERSIONS, PORT_CONTRACT_VERSIONS, fail } from '../core/domain.js';
|
|
3
|
+
import { ConfigSchema } from '../config/schema.js';
|
|
4
|
+
const platforms = Object.freeze(['windows-x64', 'linux-x64', 'darwin-x64', 'darwin-arm64']);
|
|
5
|
+
const runtimes = Object.freeze(['node', 'bun']);
|
|
6
|
+
export function adapterTarget(runtime) {
|
|
7
|
+
if (process.platform === 'win32' && process.arch === 'x64')
|
|
8
|
+
return { platform: 'windows-x64', runtime };
|
|
9
|
+
if (process.platform === 'linux' && process.arch === 'x64')
|
|
10
|
+
return { platform: 'linux-x64', runtime };
|
|
11
|
+
if (process.platform === 'darwin' && process.arch === 'x64')
|
|
12
|
+
return { platform: 'darwin-x64', runtime };
|
|
13
|
+
if (process.platform === 'darwin' && process.arch === 'arm64')
|
|
14
|
+
return { platform: 'darwin-arm64', runtime };
|
|
15
|
+
fail('ADAPTER_INCOMPATIBLE', 503, 'The current platform is not supported by Lens v1.');
|
|
16
|
+
}
|
|
17
|
+
export function capabilityManifest(definition, effectScopes = [definition.effect]) {
|
|
18
|
+
return createManifest({
|
|
19
|
+
id: `${definition.adapterId}:${definition.id}`,
|
|
20
|
+
portVersions: { capability: PORT_CONTRACT_VERSIONS.capability },
|
|
21
|
+
capabilities: [definition.id],
|
|
22
|
+
effectScopes,
|
|
23
|
+
configSchema: { input: definition.inputSchema, output: definition.outputSchema ?? null }
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export function providerManifest(config, providerId) {
|
|
27
|
+
const provider = config.providers[providerId];
|
|
28
|
+
if (provider === undefined)
|
|
29
|
+
fail('ADAPTER_INCOMPATIBLE', 503, `Provider ${providerId} is not configured.`);
|
|
30
|
+
return createManifest({
|
|
31
|
+
id: `provider:${providerId}`,
|
|
32
|
+
portVersions: { model: PORT_CONTRACT_VERSIONS.model },
|
|
33
|
+
modelReferences: Object.keys(provider.models).sort().map((modelId) => `${providerId}/${modelId}`),
|
|
34
|
+
effectScopes: ['external'],
|
|
35
|
+
configSchema: ConfigSchema.properties.providers
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export function coreManifests(config) {
|
|
39
|
+
return [
|
|
40
|
+
coreManifest('core.access', 'access', 'external', { listen: ConfigSchema.properties.listen, tls: ConfigSchema.properties.tls }),
|
|
41
|
+
coreManifest('core.config', 'config', 'workspace_write', ConfigSchema),
|
|
42
|
+
coreManifest('core.store', 'store', 'workspace_write', ConfigSchema.properties.database),
|
|
43
|
+
coreManifest('core.events', 'event', 'read_only', ConfigSchema.properties.sse),
|
|
44
|
+
coreManifest('core.execution', 'execution', 'full', ConfigSchema.properties.worker),
|
|
45
|
+
coreManifest('core.result', 'result', 'workspace_write', { version: 1 }),
|
|
46
|
+
coreManifest('core.tokenizer', 'tokenizer', 'read_only', ConfigSchema.properties.tokenizer),
|
|
47
|
+
coreManifest('core.observability', 'observability', 'read_only', ConfigSchema.properties.observability),
|
|
48
|
+
...Object.keys(config.providers).sort().map((providerId) => providerManifest(config, providerId))
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
export function assertCapabilityManifest(manifest, definition, target) {
|
|
52
|
+
assertManifest(manifest, target);
|
|
53
|
+
if (!manifest.capabilities.includes(definition.id) || !manifest.effectScopes.includes(definition.effect)) {
|
|
54
|
+
fail('ADAPTER_INCOMPATIBLE', 503, `Adapter manifest ${manifest.id} does not cover Capability ${definition.id}.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function assertProviderManifest(manifest, reference, target) {
|
|
58
|
+
assertManifest(manifest, target);
|
|
59
|
+
if (!manifest.modelReferences.includes(reference) || !manifest.effectScopes.includes('external')) {
|
|
60
|
+
fail('ADAPTER_INCOMPATIBLE', 503, `Adapter manifest ${manifest.id} does not cover Model ${reference}.`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function assertManifestSet(manifests, target) {
|
|
64
|
+
const ids = new Set();
|
|
65
|
+
for (const manifest of manifests) {
|
|
66
|
+
assertManifest(manifest, target);
|
|
67
|
+
if (ids.has(manifest.id))
|
|
68
|
+
fail('ADAPTER_INCOMPATIBLE', 503, `Adapter manifest ${manifest.id} is duplicated.`);
|
|
69
|
+
ids.add(manifest.id);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export function manifestSetDigest(manifests, target) {
|
|
73
|
+
assertManifestSet(manifests, target);
|
|
74
|
+
return digest([...manifests].sort((left, right) => left.id.localeCompare(right.id)));
|
|
75
|
+
}
|
|
76
|
+
function coreManifest(id, port, effect, configSchema) {
|
|
77
|
+
return createManifest({ id, portVersions: { [port]: PORT_CONTRACT_VERSIONS[port] }, effectScopes: [effect], configSchema });
|
|
78
|
+
}
|
|
79
|
+
function createManifest(input) {
|
|
80
|
+
return Object.freeze({
|
|
81
|
+
id: input.id,
|
|
82
|
+
version: '1.0.0',
|
|
83
|
+
portVersions: Object.freeze({ ...input.portVersions }),
|
|
84
|
+
capabilities: Object.freeze([...(input.capabilities ?? [])]),
|
|
85
|
+
modelReferences: Object.freeze([...(input.modelReferences ?? [])]),
|
|
86
|
+
platforms,
|
|
87
|
+
runtimes,
|
|
88
|
+
effectScopes: Object.freeze([...input.effectScopes]),
|
|
89
|
+
concurrencyContractVersion: ADAPTER_CONTRACT_VERSIONS.concurrency,
|
|
90
|
+
errorContractVersion: ADAPTER_CONTRACT_VERSIONS.error,
|
|
91
|
+
cancellationContractVersion: ADAPTER_CONTRACT_VERSIONS.cancellation,
|
|
92
|
+
timeoutContractVersion: ADAPTER_CONTRACT_VERSIONS.timeout,
|
|
93
|
+
recoveryContractVersion: ADAPTER_CONTRACT_VERSIONS.recovery,
|
|
94
|
+
configSchemaDigest: `sha256:${digest(JSON.parse(JSON.stringify(input.configSchema)))}`,
|
|
95
|
+
conformanceTestVersion: ADAPTER_CONTRACT_VERSIONS.conformance
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function assertManifest(manifest, target) {
|
|
99
|
+
const validContracts = manifest.concurrencyContractVersion === ADAPTER_CONTRACT_VERSIONS.concurrency
|
|
100
|
+
&& manifest.errorContractVersion === ADAPTER_CONTRACT_VERSIONS.error
|
|
101
|
+
&& manifest.cancellationContractVersion === ADAPTER_CONTRACT_VERSIONS.cancellation
|
|
102
|
+
&& manifest.timeoutContractVersion === ADAPTER_CONTRACT_VERSIONS.timeout
|
|
103
|
+
&& manifest.recoveryContractVersion === ADAPTER_CONTRACT_VERSIONS.recovery
|
|
104
|
+
&& manifest.conformanceTestVersion === ADAPTER_CONTRACT_VERSIONS.conformance;
|
|
105
|
+
const validPorts = Object.entries(manifest.portVersions).length > 0
|
|
106
|
+
&& Object.entries(manifest.portVersions).every(([port, version]) => PORT_CONTRACT_VERSIONS[port] === version);
|
|
107
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/.test(manifest.id)
|
|
108
|
+
|| manifest.version.length === 0
|
|
109
|
+
|| !validPorts
|
|
110
|
+
|| !validContracts
|
|
111
|
+
|| !manifest.platforms.includes(target.platform)
|
|
112
|
+
|| !manifest.runtimes.includes(target.runtime)
|
|
113
|
+
|| manifest.effectScopes.length === 0
|
|
114
|
+
|| !/^sha256:[0-9a-f]{64}$/.test(manifest.configSchemaDigest)) {
|
|
115
|
+
fail('ADAPTER_INCOMPATIBLE', 503, `Adapter manifest ${manifest.id || '<unknown>'} is incompatible.`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ModelGateway, type ModelRequest, type ModelStreamEvent } from '../core/domain.js';
|
|
2
|
+
import { type ConfigRuntime } from '../config/runtime.js';
|
|
3
|
+
import type { HarnessConfig } from '../config/schema.js';
|
|
4
|
+
import { type ResolvedProfile } from './profile.js';
|
|
5
|
+
import { type AdapterTarget } from './manifests.js';
|
|
6
|
+
export interface ResolvedModel {
|
|
7
|
+
readonly reference: string;
|
|
8
|
+
readonly contextWindowTokens: number;
|
|
9
|
+
readonly profile: ResolvedProfile;
|
|
10
|
+
readonly usageReliable: boolean;
|
|
11
|
+
readonly cacheSupported: boolean;
|
|
12
|
+
readonly programmaticToolCallingSupported: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare class AiModelGateway implements ModelGateway {
|
|
15
|
+
readonly config: ConfigRuntime;
|
|
16
|
+
readonly target: AdapterTarget;
|
|
17
|
+
constructor(config: ConfigRuntime, target: AdapterTarget);
|
|
18
|
+
resolve(reference: string, config?: HarnessConfig): ResolvedModel;
|
|
19
|
+
stream(request: ModelRequest, config?: HarnessConfig): AsyncIterable<ModelStreamEvent>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
2
|
+
import { createOpenAI } from '@ai-sdk/openai';
|
|
3
|
+
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
4
|
+
import { jsonSchema, stepCountIs, streamText, tool } from 'ai';
|
|
5
|
+
import { jsonValue, toolArguments } from '../core/codec.js';
|
|
6
|
+
import { fail } from '../core/domain.js';
|
|
7
|
+
import { resolveSecret } from '../config/runtime.js';
|
|
8
|
+
import { resolveProfile } from './profile.js';
|
|
9
|
+
import { assertProviderManifest, providerManifest } from './manifests.js';
|
|
10
|
+
export class AiModelGateway {
|
|
11
|
+
config;
|
|
12
|
+
target;
|
|
13
|
+
constructor(config, target) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.target = target;
|
|
16
|
+
}
|
|
17
|
+
resolve(reference, config = this.config.current.config) {
|
|
18
|
+
const { providerId, modelId } = splitReference(reference);
|
|
19
|
+
const provider = config.providers[providerId];
|
|
20
|
+
const model = provider?.models[modelId];
|
|
21
|
+
if (provider === undefined || model === undefined)
|
|
22
|
+
fail('MODEL_NOT_FOUND', 404, `Model ${reference} is not configured.`);
|
|
23
|
+
assertProviderManifest(providerManifest(config, providerId), reference, this.target);
|
|
24
|
+
return {
|
|
25
|
+
reference,
|
|
26
|
+
contextWindowTokens: model.contextWindowTokens,
|
|
27
|
+
profile: resolveProfile(config, model.harnessProfile),
|
|
28
|
+
usageReliable: model.usageReliable ?? false,
|
|
29
|
+
cacheSupported: model.cacheSupported ?? false,
|
|
30
|
+
programmaticToolCallingSupported: model.programmaticToolCallingSupported ?? false
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
async *stream(request, config = this.config.current.config) {
|
|
34
|
+
const { providerId, modelId } = splitReference(request.model);
|
|
35
|
+
const providerConfig = config.providers[providerId];
|
|
36
|
+
const modelConfig = providerConfig?.models[modelId];
|
|
37
|
+
if (providerConfig === undefined || modelConfig === undefined)
|
|
38
|
+
fail('MODEL_NOT_FOUND', 404, `Model ${request.model} is not configured.`);
|
|
39
|
+
assertProviderManifest(providerManifest(config, providerId), request.model, this.target);
|
|
40
|
+
const resolved = createModel(providerId, modelConfig.wireModel ?? modelId, providerConfig);
|
|
41
|
+
const programmatic = request.programmaticToolCalling;
|
|
42
|
+
if (programmatic !== undefined && resolved.programmaticTool === undefined) {
|
|
43
|
+
fail('ADAPTER_INCOMPATIBLE', 503, 'The configured Provider Adapter does not support hosted Programmatic Tool Calling.');
|
|
44
|
+
}
|
|
45
|
+
const callers = new Map();
|
|
46
|
+
const ordinals = new Map();
|
|
47
|
+
const toolSet = tools(request.tools, programmatic, resolved.programmaticTool, callers, ordinals);
|
|
48
|
+
const instructions = request.messages.filter((message) => message.role === 'system').map((message) => message.content).join('\n\n');
|
|
49
|
+
const conversation = request.messages.filter((message) => message.role !== 'system');
|
|
50
|
+
const result = streamText({
|
|
51
|
+
model: resolved.model,
|
|
52
|
+
...(instructions.length === 0 ? {} : { instructions }),
|
|
53
|
+
...(conversation.length === 0 ? { prompt: '' } : { messages: conversation.map(aiMessage) }),
|
|
54
|
+
tools: toolSet,
|
|
55
|
+
...(programmatic === undefined ? {} : {
|
|
56
|
+
experimental_toolCallers: Object.fromEntries(programmatic.capabilityIds
|
|
57
|
+
.filter((capabilityId) => capabilityId !== 'programmatic_tool_run' && capabilityId !== 'capability_search')
|
|
58
|
+
.map((capabilityId) => [capabilityId, ['programmatic_tool_run', 'AI_SDK_DIRECT_TOOL_CALL']])),
|
|
59
|
+
stopWhen: stepCountIs(programmatic.maxToolCalls + 1),
|
|
60
|
+
onToolExecutionStart: (event) => {
|
|
61
|
+
const caller = providerCaller(event.toolCall.providerMetadata);
|
|
62
|
+
if (caller.parentProviderCallId !== null)
|
|
63
|
+
callers.set(event.toolCall.toolCallId, caller.parentProviderCallId);
|
|
64
|
+
}
|
|
65
|
+
}),
|
|
66
|
+
maxRetries: 0,
|
|
67
|
+
...(request.maxOutputTokens === undefined ? {} : { maxOutputTokens: request.maxOutputTokens }),
|
|
68
|
+
abortSignal: request.signal,
|
|
69
|
+
...(request.reasoningEffort === null ? {} : { providerOptions: reasoningOptions(providerId, providerConfig.adapter, request.reasoningEffort) })
|
|
70
|
+
});
|
|
71
|
+
let ordinal = 0;
|
|
72
|
+
for await (const part of result.stream) {
|
|
73
|
+
if (part.type === 'text-delta')
|
|
74
|
+
yield { type: 'text', text: part.text };
|
|
75
|
+
else if (part.type === 'reasoning-delta')
|
|
76
|
+
yield { type: 'thinking', text: part.text };
|
|
77
|
+
else if (part.type === 'tool-input-start') {
|
|
78
|
+
const toolOrdinal = ordinal++;
|
|
79
|
+
ordinals.set(part.id, toolOrdinal);
|
|
80
|
+
yield { type: 'toolStart', ordinal: toolOrdinal, id: part.id, name: part.toolName };
|
|
81
|
+
}
|
|
82
|
+
else if (part.type === 'tool-call') {
|
|
83
|
+
const toolOrdinal = ordinals.get(part.toolCallId) ?? ordinal++;
|
|
84
|
+
ordinals.set(part.toolCallId, toolOrdinal);
|
|
85
|
+
const caller = providerCaller(part.providerMetadata);
|
|
86
|
+
yield {
|
|
87
|
+
type: 'tool',
|
|
88
|
+
ordinal: toolOrdinal,
|
|
89
|
+
id: part.toolCallId,
|
|
90
|
+
name: part.toolName,
|
|
91
|
+
// Not `jsonValue`: a Provider hands back a raw JSON string when the model calls a tool that was
|
|
92
|
+
// not in the request's manifest, and storing that string poisons the Context permanently.
|
|
93
|
+
arguments: toolArguments(part.input),
|
|
94
|
+
...(caller.parentProviderCallId === null ? {} : { parentProviderCallId: caller.parentProviderCallId }),
|
|
95
|
+
...(part.providerExecuted === true ? { providerExecuted: true } : {}),
|
|
96
|
+
...(caller.programmatic ? { programmaticCaller: true } : {})
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
else if (part.type === 'tool-result') {
|
|
100
|
+
const toolOrdinal = ordinals.get(part.toolCallId) ?? ordinal++;
|
|
101
|
+
ordinals.set(part.toolCallId, toolOrdinal);
|
|
102
|
+
yield {
|
|
103
|
+
type: 'toolResult',
|
|
104
|
+
ordinal: toolOrdinal,
|
|
105
|
+
id: part.toolCallId,
|
|
106
|
+
name: part.toolName,
|
|
107
|
+
output: jsonValue(part.output),
|
|
108
|
+
...(part.providerExecuted === true ? { providerExecuted: true } : {})
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
else if (part.type === 'finish') {
|
|
112
|
+
yield {
|
|
113
|
+
type: 'usage',
|
|
114
|
+
inputTokens: part.totalUsage.inputTokens ?? 0,
|
|
115
|
+
outputTokens: part.totalUsage.outputTokens ?? 0
|
|
116
|
+
};
|
|
117
|
+
yield { type: 'end', reason: part.finishReason };
|
|
118
|
+
}
|
|
119
|
+
else if (part.type === 'error') {
|
|
120
|
+
throw part.error;
|
|
121
|
+
}
|
|
122
|
+
else if (part.type === 'abort') {
|
|
123
|
+
throw new Error(part.reason ?? 'Model request aborted.');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function reasoningOptions(providerId, adapter, effort) {
|
|
129
|
+
if (adapter === '@ai-sdk/openai')
|
|
130
|
+
return { openai: { reasoningEffort: effort } };
|
|
131
|
+
if (adapter === '@ai-sdk/anthropic')
|
|
132
|
+
return { anthropic: { effort } };
|
|
133
|
+
return { [providerId]: { reasoningEffort: effort } };
|
|
134
|
+
}
|
|
135
|
+
function createModel(providerId, modelId, config) {
|
|
136
|
+
const apiKey = resolveSecret(config.options.apiKey);
|
|
137
|
+
if (config.adapter === '@ai-sdk/openai') {
|
|
138
|
+
const provider = createOpenAI({ ...(apiKey === undefined ? {} : { apiKey }), ...(config.options.baseURL === undefined ? {} : { baseURL: config.options.baseURL }), ...(config.options.headers === undefined ? {} : { headers: config.options.headers }) });
|
|
139
|
+
return { model: provider(modelId), programmaticTool: provider.tools.programmaticToolCalling() };
|
|
140
|
+
}
|
|
141
|
+
if (config.adapter === '@ai-sdk/anthropic') {
|
|
142
|
+
return { model: createAnthropic({ ...(apiKey === undefined ? {} : { apiKey }), ...(config.options.baseURL === undefined ? {} : { baseURL: config.options.baseURL }), ...(config.options.headers === undefined ? {} : { headers: config.options.headers }) })(modelId) };
|
|
143
|
+
}
|
|
144
|
+
if (config.options.baseURL === undefined)
|
|
145
|
+
fail('CONFIG_INVALID', 503, `Provider ${providerId} requires options.baseURL.`);
|
|
146
|
+
return { model: createOpenAICompatible({ name: providerId, baseURL: config.options.baseURL, ...(apiKey === undefined ? {} : { apiKey }), ...(config.options.headers === undefined ? {} : { headers: config.options.headers }) })(modelId) };
|
|
147
|
+
}
|
|
148
|
+
function tools(definitions, programmatic, providerTool, callers, ordinals) {
|
|
149
|
+
const result = {};
|
|
150
|
+
for (const definition of definitions) {
|
|
151
|
+
if (definition.id === 'programmatic_tool_run')
|
|
152
|
+
continue;
|
|
153
|
+
const eligible = programmatic?.capabilityIds.includes(definition.id) === true;
|
|
154
|
+
const descriptor = {
|
|
155
|
+
description: definition.description,
|
|
156
|
+
inputSchema: jsonSchema(definition.inputSchema),
|
|
157
|
+
outputSchema: jsonSchema((definition.outputSchema ?? { type: 'object', additionalProperties: true }))
|
|
158
|
+
};
|
|
159
|
+
if (eligible) {
|
|
160
|
+
result[definition.id] = tool({
|
|
161
|
+
...descriptor,
|
|
162
|
+
providerOptions: { openai: { outputSchema: definition.outputSchema ?? { type: 'object', additionalProperties: true } } },
|
|
163
|
+
execute: (input, options) => {
|
|
164
|
+
const parentProviderCallId = callers.get(options.toolCallId);
|
|
165
|
+
return programmatic.execute({
|
|
166
|
+
capabilityId: definition.id,
|
|
167
|
+
arguments: jsonValue(input),
|
|
168
|
+
providerCallId: options.toolCallId,
|
|
169
|
+
ordinal: ordinals.get(options.toolCallId) ?? 0,
|
|
170
|
+
...(parentProviderCallId === undefined ? {} : { parentProviderCallId })
|
|
171
|
+
}, options.abortSignal);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
result[definition.id] = tool(descriptor);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (programmatic !== undefined && providerTool !== undefined)
|
|
180
|
+
result.programmatic_tool_run = providerTool;
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
function providerCaller(value) {
|
|
184
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
185
|
+
return { programmatic: false, parentProviderCallId: null };
|
|
186
|
+
const candidates = [value, ...Object.values(value).filter(isObject)];
|
|
187
|
+
for (const candidate of candidates) {
|
|
188
|
+
const caller = candidate.caller;
|
|
189
|
+
if (!isObject(caller) || caller.type !== 'program')
|
|
190
|
+
continue;
|
|
191
|
+
const id = caller.callerId ?? caller.caller_id ?? caller.toolId ?? caller.tool_id;
|
|
192
|
+
return { programmatic: true, parentProviderCallId: typeof id === 'string' && id.length > 0 ? id : null };
|
|
193
|
+
}
|
|
194
|
+
return { programmatic: false, parentProviderCallId: null };
|
|
195
|
+
}
|
|
196
|
+
function isObject(value) {
|
|
197
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
198
|
+
}
|
|
199
|
+
function aiMessage(message) {
|
|
200
|
+
if (message.role === 'assistant' && message.toolCalls !== undefined) {
|
|
201
|
+
return {
|
|
202
|
+
role: 'assistant',
|
|
203
|
+
content: [
|
|
204
|
+
...(message.content.length === 0 ? [] : [{ type: 'text', text: message.content }]),
|
|
205
|
+
...message.toolCalls.map((call) => ({
|
|
206
|
+
type: 'tool-call',
|
|
207
|
+
toolCallId: call.id,
|
|
208
|
+
toolName: call.name,
|
|
209
|
+
input: call.arguments
|
|
210
|
+
}))
|
|
211
|
+
]
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
if (message.role === 'tool') {
|
|
215
|
+
return {
|
|
216
|
+
role: 'tool',
|
|
217
|
+
content: [{
|
|
218
|
+
type: 'tool-result',
|
|
219
|
+
toolCallId: message.toolCallId ?? 'unknown',
|
|
220
|
+
toolName: message.toolName ?? 'unknown',
|
|
221
|
+
output: toolOutput(message.content)
|
|
222
|
+
}]
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
return { role: message.role, content: message.content };
|
|
226
|
+
}
|
|
227
|
+
function toolOutput(content) {
|
|
228
|
+
try {
|
|
229
|
+
const parsed = JSON.parse(content);
|
|
230
|
+
return { type: 'json', value: jsonValue(parsed) };
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return { type: 'text', value: content };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function splitReference(reference) {
|
|
237
|
+
const slash = reference.indexOf('/');
|
|
238
|
+
if (slash < 1 || slash === reference.length - 1)
|
|
239
|
+
fail('MODEL_REFERENCE_INVALID', 400, 'Model Reference must use provider/model.');
|
|
240
|
+
return { providerId: reference.slice(0, slash), modelId: reference.slice(slash + 1) };
|
|
241
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Json } from '../core/domain.js';
|
|
2
|
+
import type { HarnessConfig } from '../config/schema.js';
|
|
3
|
+
export interface RuntimeRecord {
|
|
4
|
+
readonly sequence: number;
|
|
5
|
+
readonly occurred_at: string;
|
|
6
|
+
readonly component: string;
|
|
7
|
+
readonly operation: string;
|
|
8
|
+
readonly outcome: string;
|
|
9
|
+
readonly duration_ms: number;
|
|
10
|
+
readonly fields: Readonly<Record<string, Json>>;
|
|
11
|
+
}
|
|
12
|
+
export declare class Observability {
|
|
13
|
+
#private;
|
|
14
|
+
constructor(root: string, config: HarnessConfig['observability']);
|
|
15
|
+
record(component: string, operation: string, outcome: string, durationMs: number, fields?: Readonly<Record<string, Json>>): void;
|
|
16
|
+
increment(name: string, amount?: number): void;
|
|
17
|
+
logs(limit: number): readonly RuntimeRecord[];
|
|
18
|
+
traces(limit: number): readonly RuntimeRecord[];
|
|
19
|
+
agentOps(limit: number): readonly RuntimeRecord[];
|
|
20
|
+
metrics(): string;
|
|
21
|
+
close(): Promise<void>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { appendFile, mkdir, readdir, rename, stat, unlink } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, resolve } from 'node:path';
|
|
3
|
+
import { now } from '../core/codec.js';
|
|
4
|
+
export class Observability {
|
|
5
|
+
#logs = [];
|
|
6
|
+
#traces = [];
|
|
7
|
+
#agentOps = [];
|
|
8
|
+
#metrics = new Map();
|
|
9
|
+
#capacity;
|
|
10
|
+
#root;
|
|
11
|
+
#logConfig;
|
|
12
|
+
#sequence = 0;
|
|
13
|
+
#writeTail = Promise.resolve();
|
|
14
|
+
#retentionChecked = false;
|
|
15
|
+
constructor(root, config) {
|
|
16
|
+
this.#root = root;
|
|
17
|
+
this.#capacity = config?.traces?.capacity ?? 2048;
|
|
18
|
+
this.#logConfig = config?.logs;
|
|
19
|
+
}
|
|
20
|
+
record(component, operation, outcome, durationMs, fields = {}) {
|
|
21
|
+
const record = {
|
|
22
|
+
sequence: ++this.#sequence,
|
|
23
|
+
occurred_at: now(),
|
|
24
|
+
component,
|
|
25
|
+
operation,
|
|
26
|
+
outcome,
|
|
27
|
+
duration_ms: durationMs,
|
|
28
|
+
fields
|
|
29
|
+
};
|
|
30
|
+
this.#push(this.#logs, record);
|
|
31
|
+
this.#push(this.#traces, record);
|
|
32
|
+
if (component === 'agent' || component === 'call')
|
|
33
|
+
this.#push(this.#agentOps, record);
|
|
34
|
+
this.increment(`${component}_${operation}_${outcome}`);
|
|
35
|
+
if (this.#logConfig?.enabled === true) {
|
|
36
|
+
this.#writeTail = this.#writeTail.then(() => this.#write(record)).catch(() => undefined);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
increment(name, amount = 1) {
|
|
40
|
+
this.#metrics.set(metricName(name), (this.#metrics.get(metricName(name)) ?? 0) + amount);
|
|
41
|
+
}
|
|
42
|
+
logs(limit) {
|
|
43
|
+
return this.#logs.slice(-limit);
|
|
44
|
+
}
|
|
45
|
+
traces(limit) {
|
|
46
|
+
return this.#traces.slice(-limit);
|
|
47
|
+
}
|
|
48
|
+
agentOps(limit) {
|
|
49
|
+
return this.#agentOps.slice(-limit);
|
|
50
|
+
}
|
|
51
|
+
metrics() {
|
|
52
|
+
return [...this.#metrics.entries()].sort(([left], [right]) => left.localeCompare(right))
|
|
53
|
+
.map(([name, value]) => `# TYPE lens_${name} counter\nlens_${name} ${value}`)
|
|
54
|
+
.join('\n') + '\n';
|
|
55
|
+
}
|
|
56
|
+
async close() {
|
|
57
|
+
await this.#writeTail;
|
|
58
|
+
}
|
|
59
|
+
#push(target, record) {
|
|
60
|
+
target.push(record);
|
|
61
|
+
if (target.length > this.#capacity)
|
|
62
|
+
target.splice(0, target.length - this.#capacity);
|
|
63
|
+
}
|
|
64
|
+
async #write(record) {
|
|
65
|
+
const config = this.#logConfig;
|
|
66
|
+
if (config === undefined)
|
|
67
|
+
return;
|
|
68
|
+
const path = resolve(this.#root, config.path);
|
|
69
|
+
await mkdir(dirname(path), { recursive: true });
|
|
70
|
+
if (!this.#retentionChecked) {
|
|
71
|
+
this.#retentionChecked = true;
|
|
72
|
+
await pruneLogs(path, config.retentionDays);
|
|
73
|
+
}
|
|
74
|
+
const size = await stat(path).then((entry) => entry.size, () => 0);
|
|
75
|
+
if (size >= config.maxFileBytes) {
|
|
76
|
+
if (config.maxFiles === 1)
|
|
77
|
+
await unlink(path).catch(() => undefined);
|
|
78
|
+
else
|
|
79
|
+
await unlink(`${path}.${config.maxFiles - 1}`).catch(() => undefined);
|
|
80
|
+
for (let index = config.maxFiles - 2; index >= 1; index -= 1) {
|
|
81
|
+
await rename(`${path}.${index}`, `${path}.${index + 1}`).catch(() => undefined);
|
|
82
|
+
}
|
|
83
|
+
if (config.maxFiles > 1)
|
|
84
|
+
await rename(path, `${path}.1`).catch(() => undefined);
|
|
85
|
+
}
|
|
86
|
+
await appendFile(path, `${JSON.stringify(record)}\n`, 'utf8');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function pruneLogs(path, retentionDays) {
|
|
90
|
+
const threshold = Date.now() - retentionDays * 86400000;
|
|
91
|
+
const directory = dirname(path);
|
|
92
|
+
const name = basename(path);
|
|
93
|
+
for (const entry of await readdir(directory).catch(() => [])) {
|
|
94
|
+
if (entry !== name && !entry.startsWith(`${name}.`))
|
|
95
|
+
continue;
|
|
96
|
+
const candidate = resolve(directory, entry);
|
|
97
|
+
const modified = await stat(candidate).then((value) => value.mtimeMs, () => Date.now());
|
|
98
|
+
if (modified < threshold)
|
|
99
|
+
await unlink(candidate).catch(() => undefined);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function metricName(value) {
|
|
103
|
+
return value.toLowerCase().replace(/[^a-z0-9_]/g, '_');
|
|
104
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type EffectScope, type Json, type Policy } from '../core/domain.js';
|
|
2
|
+
export interface PolicyDecision {
|
|
3
|
+
readonly state: 'WAITING' | 'QUEUED';
|
|
4
|
+
readonly waitReason?: 'user_input' | 'policy_review';
|
|
5
|
+
readonly autoReview: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function decidePolicy(policy: Policy, effect: EffectScope): PolicyDecision;
|
|
8
|
+
export declare function policyPrompt(capabilityId: string): Json;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { fail } from '../core/domain.js';
|
|
2
|
+
export function decidePolicy(policy, effect) {
|
|
3
|
+
if (policy === 'ask')
|
|
4
|
+
return { state: 'WAITING', waitReason: 'user_input', autoReview: false };
|
|
5
|
+
if (policy === 'auto_approve')
|
|
6
|
+
return { state: 'WAITING', waitReason: 'policy_review', autoReview: true };
|
|
7
|
+
if (policy === 'full_access')
|
|
8
|
+
return { state: 'QUEUED', autoReview: false };
|
|
9
|
+
if (rank(effect) <= rank(policyEffect(policy)))
|
|
10
|
+
return { state: 'QUEUED', autoReview: false };
|
|
11
|
+
fail('POLICY_DENIED', 403, `Policy ${policy} does not allow effect ${effect}.`);
|
|
12
|
+
}
|
|
13
|
+
export function policyPrompt(capabilityId) {
|
|
14
|
+
return {
|
|
15
|
+
kind: 'policy_approval',
|
|
16
|
+
question: `Approve the Capability call ${capabilityId}?`,
|
|
17
|
+
options: [{ label: 'Approve', value: 'approve' }, { label: 'Reject', value: 'reject' }],
|
|
18
|
+
allow_freeform: false,
|
|
19
|
+
multi_select: false
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function policyEffect(policy) {
|
|
23
|
+
if (policy === 'read_only')
|
|
24
|
+
return 'read_only';
|
|
25
|
+
if (policy === 'workspace_write')
|
|
26
|
+
return 'workspace_write';
|
|
27
|
+
return 'external';
|
|
28
|
+
}
|
|
29
|
+
function rank(effect) {
|
|
30
|
+
return ['read_only', 'workspace_write', 'external', 'full'].indexOf(effect);
|
|
31
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { HarnessConfig, ModelHarnessProfile } from '../config/schema.js';
|
|
2
|
+
export interface ResolvedProfile {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly digest: string;
|
|
5
|
+
readonly value: ModelHarnessProfile;
|
|
6
|
+
}
|
|
7
|
+
export declare function resolveProfile(config: HarnessConfig, id?: string): ResolvedProfile;
|
|
8
|
+
export declare function resolveReasoningEffort(profile: ResolvedProfile, configured: string | null): string;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { digest } from '../core/codec.js';
|
|
2
|
+
import { fail } from '../core/domain.js';
|
|
3
|
+
const genericProfile = {
|
|
4
|
+
version: 1,
|
|
5
|
+
systemPrompt: {
|
|
6
|
+
sourceVersion: 'lens-agent-v1',
|
|
7
|
+
content: 'You are a Lens Agent. Complete the user goal and use only the supplied capabilities.',
|
|
8
|
+
container: 'system'
|
|
9
|
+
},
|
|
10
|
+
context: {
|
|
11
|
+
segmentOrder: ['system', 'context', 'tools'],
|
|
12
|
+
preserveProviderVisibleOrder: true,
|
|
13
|
+
allowExternalTransforms: false
|
|
14
|
+
},
|
|
15
|
+
semantics: {
|
|
16
|
+
reasoningStateRoundTrip: 'optional',
|
|
17
|
+
thinkingContinuation: 'optional',
|
|
18
|
+
toolResultOrder: 'call_ordinal',
|
|
19
|
+
structuredOutput: 'supported',
|
|
20
|
+
toolDescriptionStyle: 'plain',
|
|
21
|
+
recoveryConstraints: { partialResponseResume: 'forbidden', preserveReasoningState: false }
|
|
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
|
+
cache: {
|
|
31
|
+
enabledByDefault: false,
|
|
32
|
+
minimumEligiblePrefixTokens: 1024,
|
|
33
|
+
maximumBreakpoints: 4,
|
|
34
|
+
segmentStability: { system: 'stable', context: 'dynamic', tools: 'stable' }
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
export function resolveProfile(config, id = 'generic-v1') {
|
|
38
|
+
const value = id === 'generic-v1' ? genericProfile : config.modelHarnessProfiles?.[id];
|
|
39
|
+
if (value === undefined)
|
|
40
|
+
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
|
+
return { id, digest: digest(value), value };
|
|
45
|
+
}
|
|
46
|
+
export function resolveReasoningEffort(profile, configured) {
|
|
47
|
+
const effort = configured ?? profile.value.effort.default;
|
|
48
|
+
if (!profile.value.effort.allowed.includes(effort)) {
|
|
49
|
+
fail('MODEL_EFFORT_UNSUPPORTED', 400, `Reasoning effort ${effort} is not supported by Model Harness Profile ${profile.id}.`);
|
|
50
|
+
}
|
|
51
|
+
return effort;
|
|
52
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ConfigRuntime } from '../config/runtime.js';
|
|
2
|
+
import type { Store } from '../store/store.js';
|
|
3
|
+
import type { ArtifactStore } from './artifacts.js';
|
|
4
|
+
import type { RegisteredCapability } from './capabilities.js';
|
|
5
|
+
import { CapabilityRegistry } from './capabilities.js';
|
|
6
|
+
import type { Scheduler } from './scheduler.js';
|
|
7
|
+
import type { AiModelGateway } from './model.js';
|
|
8
|
+
import type { AutoReviewer } from './review.js';
|
|
9
|
+
import { CallRuntime } from './calls.js';
|
|
10
|
+
import { ContextEngine } from './context.js';
|
|
11
|
+
import { AgentRuntime } from './agent.js';
|
|
12
|
+
import { type AdapterTarget } from './manifests.js';
|
|
13
|
+
import type { EventBus } from './events.js';
|
|
14
|
+
export interface ProjectRuntime {
|
|
15
|
+
readonly projectId: string;
|
|
16
|
+
readonly registry: CapabilityRegistry;
|
|
17
|
+
readonly calls: CallRuntime;
|
|
18
|
+
readonly context: ContextEngine;
|
|
19
|
+
readonly agents: AgentRuntime;
|
|
20
|
+
contractDigest(): string;
|
|
21
|
+
refreshSkills(): void;
|
|
22
|
+
}
|
|
23
|
+
export declare class ProjectRuntimes {
|
|
24
|
+
#private;
|
|
25
|
+
readonly store: Store;
|
|
26
|
+
readonly config: ConfigRuntime;
|
|
27
|
+
readonly scheduler: Scheduler;
|
|
28
|
+
readonly artifacts: ArtifactStore;
|
|
29
|
+
readonly models: AiModelGateway;
|
|
30
|
+
readonly reviewer: AutoReviewer;
|
|
31
|
+
readonly sharedCapabilities: readonly RegisteredCapability[];
|
|
32
|
+
readonly target: AdapterTarget;
|
|
33
|
+
readonly events: EventBus;
|
|
34
|
+
readonly pinnedProjectId: string;
|
|
35
|
+
constructor(store: Store, config: ConfigRuntime, scheduler: Scheduler, artifacts: ArtifactStore, models: AiModelGateway, reviewer: AutoReviewer, sharedCapabilities: readonly RegisteredCapability[], target: AdapterTarget, events: EventBus, pinnedProjectId: string);
|
|
36
|
+
get(projectId: string): ProjectRuntime;
|
|
37
|
+
refresh(projectId: string): void;
|
|
38
|
+
startIdleEviction(maxIdleMs?: number): void;
|
|
39
|
+
close(): Promise<void>;
|
|
40
|
+
}
|