@geraldmaron/construct 1.0.19 → 1.0.20
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 +2 -0
- package/bin/construct +79 -0
- package/lib/cli-commands.mjs +24 -1
- package/lib/config/schema.mjs +31 -1
- package/lib/decisions/enforced-baseline.json +2 -0
- package/lib/document-ingest.mjs +71 -6
- package/lib/embedded-contract/capability.mjs +3 -3
- package/lib/embedded-contract/contract-version.mjs +1 -1
- package/lib/embedded-contract/execution.mjs +184 -0
- package/lib/embedded-contract/index.mjs +16 -0
- package/lib/embedded-contract/ingest.mjs +61 -7
- package/lib/hooks/_lib/output-mode.mjs +101 -0
- package/lib/hooks/session-start.mjs +13 -1
- package/lib/ingest/provider-extract.mjs +200 -0
- package/lib/ingest/strategy.mjs +95 -0
- package/lib/mcp/server.mjs +72 -4
- package/lib/mcp/tools/embedded-contract.mjs +17 -1
- package/lib/orchestration/run-store.mjs +82 -0
- package/lib/orchestration/runtime.mjs +240 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -159,9 +159,11 @@ The embed daemon writes its supervisor stdout log to `~/.cx/runtime/embed-daemon
|
|
|
159
159
|
|---|---|
|
|
160
160
|
| `construct capability` | Describe what this Construct install can do (embedded contract; read-only, secret-free) |
|
|
161
161
|
| `construct claude:allow` | Manage Claude Code `permissions.allow` from the outside (auto-classifier blocks the agent from editing it) |
|
|
162
|
+
| `construct execution` | Resolve the execution-capability contract for an embedded workflow (orchestrated vs prompt-only; descriptive, not enforced) |
|
|
162
163
|
| `construct hosts` | Show host support for Construct orchestration |
|
|
163
164
|
| `construct mcp` | Manage MCP integrations |
|
|
164
165
|
| `construct models` | Show or update model tier assignments |
|
|
166
|
+
| `construct orchestrate` | Construct-owned local orchestration runtime (Mode-A: single-process, filesystem-backed, no Docker) |
|
|
165
167
|
| `construct plugin` | Manage external Construct plugin manifests |
|
|
166
168
|
|
|
167
169
|
### Integrations
|
package/bin/construct
CHANGED
|
@@ -3335,6 +3335,83 @@ async function cmdValidate() {
|
|
|
3335
3335
|
process.exit(1);
|
|
3336
3336
|
}
|
|
3337
3337
|
|
|
3338
|
+
async function cmdExecution(args) {
|
|
3339
|
+
if (args[0] === 'resolve') {
|
|
3340
|
+
const flag = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : undefined; };
|
|
3341
|
+
const { resolveExecution } = await import('../lib/embedded-contract/execution.mjs');
|
|
3342
|
+
const { wrapContractResult } = await import('../lib/embedded-contract/envelope.mjs');
|
|
3343
|
+
const request = {
|
|
3344
|
+
workflowType: flag('--workflow-type'),
|
|
3345
|
+
requestedStrategy: flag('--strategy'),
|
|
3346
|
+
useConstruct: !args.includes('--no-construct'),
|
|
3347
|
+
host: flag('--host'),
|
|
3348
|
+
hostModel: flag('--host-model'),
|
|
3349
|
+
hostProvider: flag('--host-provider'),
|
|
3350
|
+
requestedTier: flag('--tier'),
|
|
3351
|
+
capabilities: flag('--capabilities')?.split(',').map((s) => s.trim()).filter(Boolean),
|
|
3352
|
+
allowCrossProviderFallback: args.includes('--allow-cross-provider'),
|
|
3353
|
+
};
|
|
3354
|
+
const envelope = wrapContractResult(resolveExecution(request), { surface: 'cli' });
|
|
3355
|
+
println(JSON.stringify(envelope, null, 2));
|
|
3356
|
+
return;
|
|
3357
|
+
}
|
|
3358
|
+
println('Usage: construct execution resolve [--workflow-type T] [--strategy orchestrated|prompt-only|auto] [--host-model M] [--host-provider P] [--tier reasoning|standard|fast] [--no-construct] [--allow-cross-provider] [--json]');
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3361
|
+
async function cmdOrchestrate(args) {
|
|
3362
|
+
const sub = args[0];
|
|
3363
|
+
const rest = args.slice(1);
|
|
3364
|
+
const flag = (name) => { const i = rest.indexOf(name); return i !== -1 ? rest[i + 1] : undefined; };
|
|
3365
|
+
const wantsJson = rest.includes('--json') || args.includes('--json');
|
|
3366
|
+
|
|
3367
|
+
if (sub === 'run') {
|
|
3368
|
+
const { runOrchestration, planRun, hostAdapterMetadata } = await import('../lib/orchestration/runtime.mjs');
|
|
3369
|
+
const positional = rest.filter((a, i) => !a.startsWith('--') && !(i > 0 && rest[i - 1].startsWith('--') && !['--no-construct', '--no-execute', '--json'].includes(rest[i - 1])));
|
|
3370
|
+
const text = flag('--text') || positional.join(' ').trim();
|
|
3371
|
+
if (!text) { errorln('Usage: construct orchestrate run "<request>" [--workflow-type T] [--strategy orchestrated|prompt-only|auto] [--host H] [--host-model M] [--no-construct] [--no-execute] [--json]'); process.exit(1); }
|
|
3372
|
+
const request = {
|
|
3373
|
+
request: text,
|
|
3374
|
+
workflowType: flag('--workflow-type'),
|
|
3375
|
+
requestedStrategy: flag('--strategy') || 'auto',
|
|
3376
|
+
useConstruct: !rest.includes('--no-construct'),
|
|
3377
|
+
host: flag('--host'),
|
|
3378
|
+
hostModel: flag('--host-model'),
|
|
3379
|
+
hostProvider: flag('--host-provider'),
|
|
3380
|
+
fileCount: Number(flag('--file-count') || 0),
|
|
3381
|
+
moduleCount: Number(flag('--module-count') || 0),
|
|
3382
|
+
};
|
|
3383
|
+
const run = rest.includes('--no-execute') ? planRun(request, {}) : runOrchestration(request, {});
|
|
3384
|
+
if (wantsJson) { println(JSON.stringify(hostAdapterMetadata(run), null, 2)); return; }
|
|
3385
|
+
println(`Run ${run.runId} — ${run.status} · executionMode=${run.execution.executionMode} · backend=${run.workerBackend} · hostRole=${run.hostRole}`);
|
|
3386
|
+
if (run.execution.degraded) println(` degraded: ${run.execution.degradationReason}`);
|
|
3387
|
+
if (run.tasks.length) println(` tasks: ${run.tasks.map((t) => `${t.role}(${t.status})`).join(' → ')}`);
|
|
3388
|
+
else println(' tasks: none (no Construct-owned specialist sequence for this execution mode)');
|
|
3389
|
+
return;
|
|
3390
|
+
}
|
|
3391
|
+
|
|
3392
|
+
if (sub === 'status') {
|
|
3393
|
+
const { getRun, getRuns, hostAdapterMetadata } = await import('../lib/orchestration/runtime.mjs');
|
|
3394
|
+
const runId = rest.find((a) => !a.startsWith('--'));
|
|
3395
|
+
if (runId) {
|
|
3396
|
+
const run = getRun(process.cwd(), runId);
|
|
3397
|
+
if (!run) { errorln(`Run not found: ${runId}`); process.exit(1); }
|
|
3398
|
+
if (wantsJson) { println(JSON.stringify(hostAdapterMetadata(run), null, 2)); return; }
|
|
3399
|
+
println(`Run ${run.runId} — ${run.status} · executionMode=${run.execution.executionMode}`);
|
|
3400
|
+
for (const t of run.tasks) println(` ${t.id} ${t.role} — ${t.status}`);
|
|
3401
|
+
return;
|
|
3402
|
+
}
|
|
3403
|
+
const runs = getRuns(process.cwd());
|
|
3404
|
+
if (wantsJson) { println(JSON.stringify(runs, null, 2)); return; }
|
|
3405
|
+
if (!runs.length) { println('No orchestration runs recorded.'); return; }
|
|
3406
|
+
for (const r of runs) println(` ${r.runId} — ${r.status} · ${r.executionMode || ''} · ${r.createdAt}`);
|
|
3407
|
+
return;
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3410
|
+
println('Usage: construct orchestrate <run|status> [options]');
|
|
3411
|
+
println(' run "<request>" [--workflow-type T] [--strategy S] [--host H] [--host-model M] [--no-construct] [--no-execute] [--json]');
|
|
3412
|
+
println(' status [run-id] [--json]');
|
|
3413
|
+
}
|
|
3414
|
+
|
|
3338
3415
|
async function cmdModels(args) {
|
|
3339
3416
|
if (args[0] === 'resolve') {
|
|
3340
3417
|
const flag = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : undefined; };
|
|
@@ -5040,6 +5117,8 @@ const handlers = new Map([
|
|
|
5040
5117
|
['docs:reconcile', cmdDocsReconcile],
|
|
5041
5118
|
['init:update', cmdInitUpdate],
|
|
5042
5119
|
['models', cmdModels],
|
|
5120
|
+
['execution', cmdExecution],
|
|
5121
|
+
['orchestrate', cmdOrchestrate],
|
|
5043
5122
|
['beads:stats', async (args) => {
|
|
5044
5123
|
const { getContentionStats, getHumanStatus } = await import('../lib/beads-optimistic.mjs');
|
|
5045
5124
|
const jsonOutput = args.includes('--json');
|
package/lib/cli-commands.mjs
CHANGED
|
@@ -250,7 +250,7 @@ export const CLI_COMMANDS = [
|
|
|
250
250
|
category: 'Work',
|
|
251
251
|
core: false,
|
|
252
252
|
description: 'Convert documents to indexed markdown',
|
|
253
|
-
usage: 'construct ingest <file> [--strict] [--legacy-extractor]',
|
|
253
|
+
usage: 'construct ingest <file> [--strategy=adapter|provider] [--strict] [--legacy-extractor]',
|
|
254
254
|
},
|
|
255
255
|
{
|
|
256
256
|
name: 'infer',
|
|
@@ -323,6 +323,29 @@ export const CLI_COMMANDS = [
|
|
|
323
323
|
{ name: 'describe --json', desc: 'Emit versions, interfaces, roles, skills, workflows, schemas, models, policies, telemetry, plugins' },
|
|
324
324
|
],
|
|
325
325
|
},
|
|
326
|
+
{
|
|
327
|
+
name: 'execution',
|
|
328
|
+
emoji: '🪢',
|
|
329
|
+
category: 'Models & Integrations',
|
|
330
|
+
core: false,
|
|
331
|
+
description: 'Resolve the execution-capability contract for an embedded workflow (orchestrated vs prompt-only; descriptive, not enforced)',
|
|
332
|
+
usage: 'construct execution resolve --json',
|
|
333
|
+
subcommands: [
|
|
334
|
+
{ name: 'resolve --json', desc: 'Report executionMode, active Construct capabilities, and any degradation given host/strategy context' },
|
|
335
|
+
],
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
name: 'orchestrate',
|
|
339
|
+
emoji: '🎼',
|
|
340
|
+
category: 'Models & Integrations',
|
|
341
|
+
core: false,
|
|
342
|
+
description: 'Construct-owned local orchestration runtime (Mode-A: single-process, filesystem-backed, no Docker)',
|
|
343
|
+
usage: 'construct orchestrate <run|status> [options]',
|
|
344
|
+
subcommands: [
|
|
345
|
+
{ name: 'run "<request>" [--strategy S] [--host H] [--no-construct] [--no-execute] [--json]', desc: 'Plan and run a request through a Construct-owned specialist chain; returns host-adapter metadata' },
|
|
346
|
+
{ name: 'status [run-id] [--json]', desc: 'Inspect a run, or list recent runs' },
|
|
347
|
+
],
|
|
348
|
+
},
|
|
326
349
|
{
|
|
327
350
|
name: 'mcp',
|
|
328
351
|
emoji: '🔌',
|
package/lib/config/schema.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* AJV/zod to keep Construct dependency-free at startup — the loader runs
|
|
6
6
|
* before npm install completes in some bootstrap paths.
|
|
7
7
|
*
|
|
8
|
-
* Schema v1 covers: alias, deployment, providers, autoEmbed, telemetry.
|
|
8
|
+
* Schema v1 covers: alias, deployment, providers, autoEmbed, ingest, telemetry.
|
|
9
9
|
* Tier model selection lives in `specialists/registry.json` only — this config
|
|
10
10
|
* file is intentionally not a second edit surface for model assignments.
|
|
11
11
|
* Later phases (6, 7b) extend it with `resources` and `costs` blocks
|
|
@@ -24,6 +24,14 @@ export const DEFAULT_PROFILE_ID = 'rnd';
|
|
|
24
24
|
|
|
25
25
|
export const SURFACES = ['claude', 'opencode', 'codex', 'copilot', 'vscode', 'cursor'];
|
|
26
26
|
|
|
27
|
+
export const INGEST_STRATEGIES = ['adapter', 'provider'];
|
|
28
|
+
export const INGEST_FALLBACKS = ['none', 'provider', 'adapter'];
|
|
29
|
+
|
|
30
|
+
// SessionStart context routing. `auto` keeps the rich payload on stdout for
|
|
31
|
+
// interactive sessions and suppresses it (to a debug log) for non-interactive /
|
|
32
|
+
// SDK invocations, so a one-shot command's stdout stays reserved for its output.
|
|
33
|
+
export const HOOK_OUTPUT_MODES = ['auto', 'silent', 'stderr', 'stdout'];
|
|
34
|
+
|
|
27
35
|
export const DEFAULT_PROJECT_CONFIG = Object.freeze({
|
|
28
36
|
version: CONFIG_SCHEMA_VERSION,
|
|
29
37
|
alias: 'Construct',
|
|
@@ -36,9 +44,16 @@ export const DEFAULT_PROJECT_CONFIG = Object.freeze({
|
|
|
36
44
|
providers: Object.freeze({}),
|
|
37
45
|
profile: DEFAULT_PROFILE_ID,
|
|
38
46
|
autoEmbed: false,
|
|
47
|
+
ingest: Object.freeze({
|
|
48
|
+
strategy: 'adapter',
|
|
49
|
+
fallback: 'none',
|
|
50
|
+
}),
|
|
39
51
|
telemetry: Object.freeze({
|
|
40
52
|
enabled: true,
|
|
41
53
|
}),
|
|
54
|
+
hooks: Object.freeze({
|
|
55
|
+
outputMode: 'auto',
|
|
56
|
+
}),
|
|
42
57
|
roleSelection: Object.freeze({
|
|
43
58
|
primary: null,
|
|
44
59
|
secondary: null,
|
|
@@ -84,6 +99,14 @@ export const FIELD_RULES = {
|
|
|
84
99
|
providers: { type: 'object', required: false },
|
|
85
100
|
profile: { type: 'string', required: false, maxLength: 40 },
|
|
86
101
|
autoEmbed: { type: 'boolean', required: false },
|
|
102
|
+
ingest: {
|
|
103
|
+
type: 'object',
|
|
104
|
+
required: false,
|
|
105
|
+
fields: {
|
|
106
|
+
strategy: { type: 'string', enum: INGEST_STRATEGIES },
|
|
107
|
+
fallback: { type: 'string', enum: INGEST_FALLBACKS },
|
|
108
|
+
},
|
|
109
|
+
},
|
|
87
110
|
telemetry: {
|
|
88
111
|
type: 'object',
|
|
89
112
|
required: false,
|
|
@@ -91,6 +114,13 @@ export const FIELD_RULES = {
|
|
|
91
114
|
enabled: { type: 'boolean' },
|
|
92
115
|
},
|
|
93
116
|
},
|
|
117
|
+
hooks: {
|
|
118
|
+
type: 'object',
|
|
119
|
+
required: false,
|
|
120
|
+
fields: {
|
|
121
|
+
outputMode: { type: 'string', enum: HOOK_OUTPUT_MODES },
|
|
122
|
+
},
|
|
123
|
+
},
|
|
94
124
|
roleSelection: {
|
|
95
125
|
type: 'object',
|
|
96
126
|
required: false,
|
package/lib/document-ingest.mjs
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Reuses the shared extraction backends from lib/document-extract.mjs, writes
|
|
5
5
|
* markdown outputs into retrieval-friendly project paths, and can optionally
|
|
6
|
-
* trigger storage sync for SQL/vector indexing.
|
|
6
|
+
* trigger storage sync for SQL/vector indexing. The extraction step honors a
|
|
7
|
+
* resolved ingest strategy (adapter | provider) with an explicit fallback policy
|
|
8
|
+
* and records the selected strategy/model in the returned `ingestion` block.
|
|
7
9
|
*/
|
|
8
10
|
import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
9
11
|
import { basename, dirname, extname, isAbsolute, join, parse, relative, resolve } from 'node:path';
|
|
@@ -11,6 +13,9 @@ import { extractDocumentText, extractDocumentTextAsync, extractDocumentMetadata,
|
|
|
11
13
|
import { syncFileStateToSql } from './storage/sync.mjs';
|
|
12
14
|
import { stampFrontmatter } from './doc-stamp.mjs';
|
|
13
15
|
import { KNOWLEDGE_ROOT, KNOWLEDGE_SUBDIRS } from './knowledge/layout.mjs';
|
|
16
|
+
import { loadProjectConfig } from './config/project-config.mjs';
|
|
17
|
+
import { resolveIngestStrategy, INGEST_STRATEGIES } from './ingest/strategy.mjs';
|
|
18
|
+
import { extractViaProvider } from './ingest/provider-extract.mjs';
|
|
14
19
|
|
|
15
20
|
const DEFAULT_TARGET_DIR = '.cx/knowledge/internal';
|
|
16
21
|
|
|
@@ -121,6 +126,38 @@ function resolveOutputPath(sourcePath, { cwd, outputPath, outputDir, target }) {
|
|
|
121
126
|
return join(resolvedDir, `${basename(sourcePath)}.md`);
|
|
122
127
|
}
|
|
123
128
|
|
|
129
|
+
function extractViaAdapter(sourcePath, { highFidelity, maxChars }) {
|
|
130
|
+
return highFidelity
|
|
131
|
+
? extractDocumentTextAsync(sourcePath, { maxChars })
|
|
132
|
+
: extractDocumentText(sourcePath, { maxChars });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function extractWithStrategy(sourcePath, { strategy, fallback, model, provider, highFidelity, maxChars, env }) {
|
|
136
|
+
const primary = strategy === 'provider' ? 'provider' : 'adapter';
|
|
137
|
+
const run = (mode) => (mode === 'provider'
|
|
138
|
+
? extractViaProvider({ filePath: sourcePath, model, provider, maxChars, env })
|
|
139
|
+
: extractViaAdapter(sourcePath, { highFidelity, maxChars }));
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const extracted = await run(primary);
|
|
143
|
+
return { extracted, strategyUsed: primary, fallbackApplied: null, error: null };
|
|
144
|
+
} catch (primaryErr) {
|
|
145
|
+
if (fallback === 'none' || fallback === primary) {
|
|
146
|
+
const err = new Error(`ingest ${primary} strategy failed and fallback is "${fallback}": ${primaryErr.message}`);
|
|
147
|
+
err.code = primaryErr.code || 'INGEST_STRATEGY_FAILED';
|
|
148
|
+
err.cause = primaryErr;
|
|
149
|
+
throw err;
|
|
150
|
+
}
|
|
151
|
+
const extracted = await run(fallback);
|
|
152
|
+
return {
|
|
153
|
+
extracted,
|
|
154
|
+
strategyUsed: fallback,
|
|
155
|
+
fallbackApplied: { from: primary, to: fallback, reason: primaryErr.message, code: primaryErr.code || 'INGEST_STRATEGY_FAILED' },
|
|
156
|
+
error: null,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
124
161
|
export async function ingestDocuments(inputPaths, {
|
|
125
162
|
cwd = process.cwd(),
|
|
126
163
|
outputPath = null,
|
|
@@ -129,6 +166,7 @@ export async function ingestDocuments(inputPaths, {
|
|
|
129
166
|
sync = false,
|
|
130
167
|
strict = false,
|
|
131
168
|
highFidelity = true,
|
|
169
|
+
strategy = null,
|
|
132
170
|
env = process.env,
|
|
133
171
|
} = {}) {
|
|
134
172
|
if (!Array.isArray(inputPaths) || inputPaths.length === 0) {
|
|
@@ -143,12 +181,24 @@ export async function ingestDocuments(inputPaths, {
|
|
|
143
181
|
throw new Error('No supported document files found');
|
|
144
182
|
}
|
|
145
183
|
|
|
184
|
+
const { config } = loadProjectConfig(cwd, env);
|
|
185
|
+
const resolvedStrategy = resolveIngestStrategy({ config, env, override: strategy });
|
|
186
|
+
|
|
146
187
|
const results = [];
|
|
147
188
|
let totalDrops = 0;
|
|
189
|
+
let fallbackApplied = null;
|
|
148
190
|
for (const sourcePath of files) {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
:
|
|
191
|
+
const routed = await extractWithStrategy(sourcePath, {
|
|
192
|
+
strategy: resolvedStrategy.strategy,
|
|
193
|
+
fallback: resolvedStrategy.fallback,
|
|
194
|
+
model: resolvedStrategy.model,
|
|
195
|
+
provider: resolvedStrategy.provider,
|
|
196
|
+
highFidelity,
|
|
197
|
+
maxChars: 200_000,
|
|
198
|
+
env,
|
|
199
|
+
});
|
|
200
|
+
const extracted = routed.extracted;
|
|
201
|
+
if (routed.fallbackApplied) fallbackApplied = routed.fallbackApplied;
|
|
152
202
|
totalDrops += (extracted.droppedInfo ?? []).reduce((a, d) => a + (Number(d.count) || 1), 0);
|
|
153
203
|
const metadata = extractDocumentMetadata(sourcePath);
|
|
154
204
|
const targetPath = resolveOutputPath(sourcePath, { cwd, outputPath, outputDir, target });
|
|
@@ -213,6 +263,13 @@ export async function ingestDocuments(inputPaths, {
|
|
|
213
263
|
droppedInfo: results.flatMap((r) => r.droppedInfo.map((d) => ({ ...d, sourcePath: r.sourcePath }))),
|
|
214
264
|
droppedCount: totalDrops,
|
|
215
265
|
highFidelity,
|
|
266
|
+
ingestion: {
|
|
267
|
+
strategy: resolvedStrategy.strategy,
|
|
268
|
+
fallback: resolvedStrategy.fallback,
|
|
269
|
+
model: resolvedStrategy.model,
|
|
270
|
+
provider: resolvedStrategy.provider,
|
|
271
|
+
fallbackApplied,
|
|
272
|
+
},
|
|
216
273
|
};
|
|
217
274
|
}
|
|
218
275
|
|
|
@@ -224,26 +281,34 @@ export async function runIngestCli(argv = process.argv.slice(2), { cwd = process
|
|
|
224
281
|
let sync = false;
|
|
225
282
|
let strict = false;
|
|
226
283
|
let highFidelity = true;
|
|
284
|
+
let strategy = null;
|
|
227
285
|
|
|
228
286
|
for (const arg of argv) {
|
|
229
287
|
if (arg.startsWith('--out=')) outputPath = arg.split('=').slice(1).join('=');
|
|
230
288
|
else if (arg.startsWith('--out-dir=')) outputDir = arg.split('=').slice(1).join('=');
|
|
231
289
|
else if (arg.startsWith('--target=')) target = arg.split('=').slice(1).join('=');
|
|
290
|
+
else if (arg.startsWith('--strategy=')) strategy = arg.split('=').slice(1).join('=');
|
|
291
|
+
else if (arg === '--strategy') strategy = '';
|
|
232
292
|
else if (arg === '--sync') sync = true;
|
|
233
293
|
else if (arg === '--strict') strict = true;
|
|
234
294
|
else if (arg === '--legacy-extractor') highFidelity = false;
|
|
295
|
+
else if (strategy === '') strategy = arg;
|
|
235
296
|
else inputs.push(arg);
|
|
236
297
|
}
|
|
237
298
|
|
|
238
299
|
if (inputs.length === 0) {
|
|
239
300
|
throw new Error(
|
|
240
301
|
`Usage: construct ingest <file-or-dir> [more paths] [--out=FILE] [--out-dir=DIR] ` +
|
|
241
|
-
`[--target=sibling|knowledge/<subdir>] [--sync] [--strict] [--legacy-extractor]\n` +
|
|
302
|
+
`[--target=sibling|knowledge/<subdir>] [--strategy=adapter|provider] [--sync] [--strict] [--legacy-extractor]\n` +
|
|
303
|
+
` --strategy: extraction strategy override (adapter = local extractors; provider = configured provider/model)\n` +
|
|
242
304
|
` --strict: exit non-zero if any extraction drops occur\n` +
|
|
243
305
|
` --legacy-extractor: use the pre-docling regex extractor (lower fidelity)\n` +
|
|
244
306
|
` knowledge subdirs: ${KNOWLEDGE_SUBDIRS.join(', ')}`,
|
|
245
307
|
);
|
|
246
308
|
}
|
|
309
|
+
if (strategy !== null && strategy !== '' && !INGEST_STRATEGIES.includes(strategy)) {
|
|
310
|
+
throw new Error(`Unsupported strategy: ${strategy}. Valid strategies: ${INGEST_STRATEGIES.join(', ')}`);
|
|
311
|
+
}
|
|
247
312
|
const knowledgeTargets = KNOWLEDGE_SUBDIRS.map((s) => `knowledge/${s}`);
|
|
248
313
|
if (!['sibling', ...knowledgeTargets].includes(target)) {
|
|
249
314
|
throw new Error(
|
|
@@ -251,5 +316,5 @@ export async function runIngestCli(argv = process.argv.slice(2), { cwd = process
|
|
|
251
316
|
);
|
|
252
317
|
}
|
|
253
318
|
|
|
254
|
-
return ingestDocuments(inputs, { cwd, outputPath, outputDir, target, sync, strict, highFidelity, env });
|
|
319
|
+
return ingestDocuments(inputs, { cwd, outputPath, outputDir, target, sync, strict, highFidelity, strategy: strategy || null, env });
|
|
255
320
|
}
|
|
@@ -32,9 +32,9 @@ const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
|
32
32
|
|
|
33
33
|
function buildInterfaces() {
|
|
34
34
|
return [
|
|
35
|
-
{ surface: 'cli', contractVersion: CONTRACT_VERSION, entrypoints: ['construct capability describe --json', 'construct models resolve --json', 'construct intake classify --json', 'construct graph recommend --json', 'construct workflow invoke --json'] },
|
|
36
|
-
{ surface: 'mcp', contractVersion: CONTRACT_VERSION, tools: ['capability_describe', 'model_resolve', 'triage_recommend', 'workflow_invoke'] },
|
|
37
|
-
{ surface: 'sdk', contractVersion: CONTRACT_VERSION, module: 'construct/embedded-contract', exports: ['describeCapabilities', 'resolveEmbeddedModel', 'recommendPlan', 'invokeWorkflow'] },
|
|
35
|
+
{ surface: 'cli', contractVersion: CONTRACT_VERSION, entrypoints: ['construct capability describe --json', 'construct models resolve --json', 'construct execution resolve --json', 'construct intake classify --json', 'construct graph recommend --json', 'construct workflow invoke --json'] },
|
|
36
|
+
{ surface: 'mcp', contractVersion: CONTRACT_VERSION, tools: ['capability_describe', 'model_resolve', 'construct_execution_resolve', 'triage_recommend', 'workflow_invoke'] },
|
|
37
|
+
{ surface: 'sdk', contractVersion: CONTRACT_VERSION, module: 'construct/embedded-contract', exports: ['describeCapabilities', 'resolveEmbeddedModel', 'resolveExecution', 'recommendPlan', 'invokeWorkflow'] },
|
|
38
38
|
];
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { parseSemver, compareSemver } from '../version.mjs';
|
|
17
17
|
|
|
18
|
-
export const CONTRACT_VERSION = '1.
|
|
18
|
+
export const CONTRACT_VERSION = '1.1.0';
|
|
19
19
|
|
|
20
20
|
// A client built against an older minor of the same major still works, because
|
|
21
21
|
// minor bumps are additive-only; a different major is incompatible either way.
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/embedded-contract/execution.mjs — embedded execution-capability contract.
|
|
3
|
+
*
|
|
4
|
+
* An embedded host can request Construct orchestration but cannot otherwise tell
|
|
5
|
+
* whether a run actually engaged personas/skills/routing or degraded to a
|
|
6
|
+
* prompt-only envelope — and if it degraded, why. This contract answers that
|
|
7
|
+
* before or at workflow start: given a requested strategy and the host's model
|
|
8
|
+
* context, it derives an `executionMode`, the Construct capabilities that mode
|
|
9
|
+
* contributes, and an explicit degradation reason.
|
|
10
|
+
*
|
|
11
|
+
* Honesty boundary (ADR-0019): orchestration in the embedded layer is
|
|
12
|
+
* DESCRIPTIVE, not enforced. Construct returns a plan; the host runtime performs
|
|
13
|
+
* the reasoning. So this contract reports what Construct PLANNED and can RESOLVE
|
|
14
|
+
* a model for — never an observation that the host executed personas. Every
|
|
15
|
+
* response carries a `semantics` disclaimer to that effect, and
|
|
16
|
+
* `constructCapabilitiesActive` describes Construct's contribution, not proof of
|
|
17
|
+
* host execution. Claiming observed orchestration would violate the
|
|
18
|
+
* no-fabrication rule. A `same-family-fallback` (host model unavailable) and a
|
|
19
|
+
* `config-error` (no model) both surface as `degraded: true`.
|
|
20
|
+
*
|
|
21
|
+
* Pure and synchronous; reuses resolveEmbeddedModel (no second model surface)
|
|
22
|
+
* and returns a result object carrying `warnings`, lifted into the envelope by
|
|
23
|
+
* the calling surface.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { getDeploymentMode } from '../deployment-mode.mjs';
|
|
27
|
+
import { resolveEmbeddedModel } from './model-resolve.mjs';
|
|
28
|
+
import { getWorkflowDef } from './workflow-defs.mjs';
|
|
29
|
+
|
|
30
|
+
export const EXECUTION_MODES = ['construct-orchestrated', 'construct-prompt-only', 'host-direct', 'same-family-fallback'];
|
|
31
|
+
export const CONSTRUCT_STRATEGIES = ['orchestrated', 'prompt-only', 'auto'];
|
|
32
|
+
export const CONSTRUCT_CAPABILITIES = ['personas', 'skills', 'workflow-routing', 'prompt-envelope'];
|
|
33
|
+
|
|
34
|
+
export const EXECUTION_SEMANTICS = 'Reports Construct-planned capability and model-resolvability before/at workflow start; does not observe host execution. constructCapabilitiesActive describes what Construct contributes, not proof the host ran personas/skills.';
|
|
35
|
+
|
|
36
|
+
const ORCHESTRATED_CAPS = ['personas', 'skills', 'workflow-routing', 'prompt-envelope'];
|
|
37
|
+
const PROMPT_ONLY_CAPS = ['prompt-envelope'];
|
|
38
|
+
|
|
39
|
+
function normalizeStrategy(value) {
|
|
40
|
+
const lowered = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
41
|
+
return CONSTRUCT_STRATEGIES.includes(lowered) ? lowered : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the execution-capability contract for an embedded workflow.
|
|
46
|
+
*
|
|
47
|
+
* @param {object} request
|
|
48
|
+
* @param {string} [request.workflowType] workflow whose orchestration plan is being weighed
|
|
49
|
+
* @param {string} [request.requestedStrategy] orchestrated | prompt-only | auto (default auto)
|
|
50
|
+
* @param {boolean} [request.useConstruct=true] false ⇒ host-direct (host runs without Construct)
|
|
51
|
+
* @param {string} [request.host]
|
|
52
|
+
* @param {string} [request.hostModel]
|
|
53
|
+
* @param {string} [request.hostProvider]
|
|
54
|
+
* @param {string} [request.requestedTier]
|
|
55
|
+
* @param {string[]} [request.capabilities]
|
|
56
|
+
* @param {boolean} [request.allowCrossProviderFallback=false]
|
|
57
|
+
* @param {object} [opts]
|
|
58
|
+
* @param {Record<string,string>} [opts.env]
|
|
59
|
+
* @param {string} [opts.cwd]
|
|
60
|
+
* @param {string} [opts.registryPath]
|
|
61
|
+
* @returns {object}
|
|
62
|
+
*/
|
|
63
|
+
export function resolveExecution(request = {}, { env = process.env, cwd = process.cwd(), registryPath = null } = {}) {
|
|
64
|
+
const {
|
|
65
|
+
workflowType = null,
|
|
66
|
+
requestedStrategy: rawStrategy,
|
|
67
|
+
useConstruct = true,
|
|
68
|
+
host, hostModel, hostProvider, requestedTier, capabilities, allowCrossProviderFallback = false,
|
|
69
|
+
} = request;
|
|
70
|
+
|
|
71
|
+
const warnings = [];
|
|
72
|
+
const requestedStrategy = normalizeStrategy(rawStrategy) || 'auto';
|
|
73
|
+
if (rawStrategy != null && !normalizeStrategy(rawStrategy)) {
|
|
74
|
+
warnings.push(`Unknown requestedStrategy "${rawStrategy}"; defaulted to "auto".`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const deploymentMode = getDeploymentMode(env, { cwd });
|
|
78
|
+
|
|
79
|
+
// Model resolution is the single source of provider/model truth; its warnings
|
|
80
|
+
// are namespaced so an embedder can tell model concerns from execution ones.
|
|
81
|
+
const modelResolution = resolveEmbeddedModel(
|
|
82
|
+
{ workflowType, requestedTier, host, hostModel, hostProvider, capabilities, allowCrossProviderFallback },
|
|
83
|
+
{ env, registryPath },
|
|
84
|
+
);
|
|
85
|
+
for (const w of modelResolution.warnings || []) warnings.push(`model-resolution: ${w}`);
|
|
86
|
+
|
|
87
|
+
const { resolutionSource, selectedModel, selectedProvider } = modelResolution;
|
|
88
|
+
const orchestrationAvailable = resolutionSource !== 'config-error';
|
|
89
|
+
|
|
90
|
+
// An explicit-but-unknown workflowType has no plan; absent a workflowType,
|
|
91
|
+
// Construct's generic router can still orchestrate, so a plan is available.
|
|
92
|
+
const knownWorkflow = workflowType ? Boolean(getWorkflowDef(workflowType)) : true;
|
|
93
|
+
if (workflowType && !knownWorkflow) {
|
|
94
|
+
warnings.push(`Unknown workflowType "${workflowType}"; no orchestration plan is available for it.`);
|
|
95
|
+
}
|
|
96
|
+
const orchestrationPlanned = knownWorkflow;
|
|
97
|
+
|
|
98
|
+
const base = {
|
|
99
|
+
requestedStrategy,
|
|
100
|
+
selectedProvider,
|
|
101
|
+
selectedModel,
|
|
102
|
+
resolutionSource,
|
|
103
|
+
orchestrationPlanned,
|
|
104
|
+
orchestrationAvailable,
|
|
105
|
+
deploymentMode,
|
|
106
|
+
modelResolution: stripWarnings(modelResolution),
|
|
107
|
+
semantics: EXECUTION_SEMANTICS,
|
|
108
|
+
warnings,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
if (useConstruct === false) {
|
|
112
|
+
return {
|
|
113
|
+
...base,
|
|
114
|
+
executionMode: 'host-direct',
|
|
115
|
+
effectiveStrategy: 'host-direct',
|
|
116
|
+
constructCapabilitiesActive: [],
|
|
117
|
+
degraded: false,
|
|
118
|
+
degradationReason: null,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (requestedStrategy === 'prompt-only') {
|
|
123
|
+
return {
|
|
124
|
+
...base,
|
|
125
|
+
executionMode: 'construct-prompt-only',
|
|
126
|
+
effectiveStrategy: 'prompt-only',
|
|
127
|
+
constructCapabilitiesActive: [...PROMPT_ONLY_CAPS],
|
|
128
|
+
degraded: false,
|
|
129
|
+
degradationReason: null,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// orchestrated or auto: deliver orchestration only when a plan exists AND a
|
|
134
|
+
// model resolves. A same-family fallback runs but did not honor the host's
|
|
135
|
+
// exact model, so it is reported as its own mode and as degraded.
|
|
136
|
+
const canOrchestrate = orchestrationPlanned && orchestrationAvailable;
|
|
137
|
+
if (canOrchestrate) {
|
|
138
|
+
if (resolutionSource === 'same-family-fallback') {
|
|
139
|
+
return {
|
|
140
|
+
...base,
|
|
141
|
+
executionMode: 'same-family-fallback',
|
|
142
|
+
effectiveStrategy: 'orchestrated',
|
|
143
|
+
constructCapabilitiesActive: [...ORCHESTRATED_CAPS],
|
|
144
|
+
degraded: true,
|
|
145
|
+
degradationReason: `Host model unavailable; resolved a same-family tier model — ${modelResolution.fallbackReason || 'same-family fallback applied'}.`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
...base,
|
|
150
|
+
executionMode: 'construct-orchestrated',
|
|
151
|
+
effectiveStrategy: 'orchestrated',
|
|
152
|
+
constructCapabilitiesActive: [...ORCHESTRATED_CAPS],
|
|
153
|
+
degraded: false,
|
|
154
|
+
degradationReason: null,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Could not orchestrate. Degradation is asserted only when orchestration was
|
|
159
|
+
// explicitly requested, or when no model resolved at all (auto still needs a
|
|
160
|
+
// model to do anything useful).
|
|
161
|
+
let degraded = false;
|
|
162
|
+
let degradationReason = null;
|
|
163
|
+
if (!orchestrationAvailable) {
|
|
164
|
+
degraded = true;
|
|
165
|
+
degradationReason = `Model could not be resolved; orchestration requires a runnable model — ${modelResolution.error?.reason || 'no model available'}.`;
|
|
166
|
+
} else if (requestedStrategy === 'orchestrated') {
|
|
167
|
+
degraded = true;
|
|
168
|
+
degradationReason = `Orchestration was requested but no orchestration plan is available${workflowType ? ` for workflowType "${workflowType}"` : ''}.`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
...base,
|
|
173
|
+
executionMode: 'construct-prompt-only',
|
|
174
|
+
effectiveStrategy: 'prompt-only',
|
|
175
|
+
constructCapabilitiesActive: [...PROMPT_ONLY_CAPS],
|
|
176
|
+
degraded,
|
|
177
|
+
degradationReason,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function stripWarnings(result) {
|
|
182
|
+
const { warnings, ...rest } = result || {};
|
|
183
|
+
return rest;
|
|
184
|
+
}
|
|
@@ -12,9 +12,11 @@ import { resolveEmbeddedModel as resolveModelCore } from './model-resolve.mjs';
|
|
|
12
12
|
import { recommendPlan as recommendPlanCore } from './triage.mjs';
|
|
13
13
|
import { invokeWorkflow as invokeWorkflowCore } from './workflow-invoke.mjs';
|
|
14
14
|
import { buildCapabilityContract } from './capability.mjs';
|
|
15
|
+
import { resolveExecution as resolveExecutionCore } from './execution.mjs';
|
|
15
16
|
|
|
16
17
|
export { CONTRACT_VERSION, MIN_CLIENT_CONTRACT_VERSION, isClientCompatible } from './contract-version.mjs';
|
|
17
18
|
export { APPROVAL_MODES } from './audit.mjs';
|
|
19
|
+
export { EXECUTION_MODES, CONSTRUCT_STRATEGIES, CONSTRUCT_CAPABILITIES } from './execution.mjs';
|
|
18
20
|
|
|
19
21
|
// File→text resolution for SDK callers: extract a path through the real
|
|
20
22
|
// pipeline (docling/whisper/transcript), then pass { text, ingestion } to
|
|
@@ -69,3 +71,17 @@ export async function invokeWorkflow(request = {}, { env, cwd } = {}) {
|
|
|
69
71
|
export function describeCapabilities({ env, cwd, rootDir } = {}) {
|
|
70
72
|
return wrapContractResult(buildCapabilityContract({ env, rootDir }), { surface: 'sdk', env, cwd });
|
|
71
73
|
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resolve the execution-capability contract for an embedded workflow: the
|
|
77
|
+
* executionMode, the contributed Construct capabilities, and any degradation
|
|
78
|
+
* with a reason. Returns a versioned envelope; the resolution is under `.data`.
|
|
79
|
+
* Read-only; performs no model call beyond model resolution.
|
|
80
|
+
*
|
|
81
|
+
* @param {object} request See execution.mjs resolveExecution.
|
|
82
|
+
* @param {object} [opts] { env, cwd, registryPath }
|
|
83
|
+
* @returns {object}
|
|
84
|
+
*/
|
|
85
|
+
export function resolveExecution(request = {}, { env, cwd, registryPath } = {}) {
|
|
86
|
+
return wrapContractResult(resolveExecutionCore(request, { env, cwd, registryPath }), { surface: 'sdk', env, cwd });
|
|
87
|
+
}
|