@geraldmaron/construct 1.0.19 → 1.0.21
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 +49 -1
- package/lib/decisions/enforced-baseline.json +3 -0
- package/lib/document-ingest.mjs +80 -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/embedded-contract/triage.mjs +24 -2
- package/lib/embedded-contract/workflow-invoke.mjs +21 -0
- 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 +127 -0
- package/lib/mcp/server.mjs +72 -4
- package/lib/mcp/tools/embedded-contract.mjs +17 -1
- package/lib/orchestration/run-store-postgres.mjs +85 -0
- package/lib/orchestration/run-store-sqlite.mjs +122 -0
- package/lib/orchestration/run-store.mjs +82 -0
- package/lib/orchestration/runtime.mjs +317 -0
- package/lib/orchestration/store.mjs +102 -0
- package/lib/orchestration/worker.mjs +167 -0
- package/lib/parity.mjs +35 -3
- 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') ? await planRun(request, {}) : await 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 = await 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 = await 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] [--orchestration=prompt-only|orchestrated] [--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,18 @@ 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
|
+
export const INGEST_ORCHESTRATIONS = ['prompt-only', 'orchestrated'];
|
|
30
|
+
|
|
31
|
+
export const ORCHESTRATION_WORKER_BACKENDS = ['inline', 'provider'];
|
|
32
|
+
export const ORCHESTRATION_STORES = ['filesystem', 'sqlite', 'postgres'];
|
|
33
|
+
|
|
34
|
+
// SessionStart context routing. `auto` keeps the rich payload on stdout for
|
|
35
|
+
// interactive sessions and suppresses it (to a debug log) for non-interactive /
|
|
36
|
+
// SDK invocations, so a one-shot command's stdout stays reserved for its output.
|
|
37
|
+
export const HOOK_OUTPUT_MODES = ['auto', 'silent', 'stderr', 'stdout'];
|
|
38
|
+
|
|
27
39
|
export const DEFAULT_PROJECT_CONFIG = Object.freeze({
|
|
28
40
|
version: CONFIG_SCHEMA_VERSION,
|
|
29
41
|
alias: 'Construct',
|
|
@@ -36,9 +48,21 @@ export const DEFAULT_PROJECT_CONFIG = Object.freeze({
|
|
|
36
48
|
providers: Object.freeze({}),
|
|
37
49
|
profile: DEFAULT_PROFILE_ID,
|
|
38
50
|
autoEmbed: false,
|
|
51
|
+
ingest: Object.freeze({
|
|
52
|
+
strategy: 'adapter',
|
|
53
|
+
fallback: 'none',
|
|
54
|
+
orchestration: 'prompt-only',
|
|
55
|
+
}),
|
|
56
|
+
orchestration: Object.freeze({
|
|
57
|
+
workerBackend: 'inline',
|
|
58
|
+
store: 'filesystem',
|
|
59
|
+
}),
|
|
39
60
|
telemetry: Object.freeze({
|
|
40
61
|
enabled: true,
|
|
41
62
|
}),
|
|
63
|
+
hooks: Object.freeze({
|
|
64
|
+
outputMode: 'auto',
|
|
65
|
+
}),
|
|
42
66
|
roleSelection: Object.freeze({
|
|
43
67
|
primary: null,
|
|
44
68
|
secondary: null,
|
|
@@ -84,6 +108,23 @@ export const FIELD_RULES = {
|
|
|
84
108
|
providers: { type: 'object', required: false },
|
|
85
109
|
profile: { type: 'string', required: false, maxLength: 40 },
|
|
86
110
|
autoEmbed: { type: 'boolean', required: false },
|
|
111
|
+
ingest: {
|
|
112
|
+
type: 'object',
|
|
113
|
+
required: false,
|
|
114
|
+
fields: {
|
|
115
|
+
strategy: { type: 'string', enum: INGEST_STRATEGIES },
|
|
116
|
+
fallback: { type: 'string', enum: INGEST_FALLBACKS },
|
|
117
|
+
orchestration: { type: 'string', enum: INGEST_ORCHESTRATIONS },
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
orchestration: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
required: false,
|
|
123
|
+
fields: {
|
|
124
|
+
workerBackend: { type: 'string', enum: ORCHESTRATION_WORKER_BACKENDS },
|
|
125
|
+
store: { type: 'string', enum: ORCHESTRATION_STORES },
|
|
126
|
+
},
|
|
127
|
+
},
|
|
87
128
|
telemetry: {
|
|
88
129
|
type: 'object',
|
|
89
130
|
required: false,
|
|
@@ -91,6 +132,13 @@ export const FIELD_RULES = {
|
|
|
91
132
|
enabled: { type: 'boolean' },
|
|
92
133
|
},
|
|
93
134
|
},
|
|
135
|
+
hooks: {
|
|
136
|
+
type: 'object',
|
|
137
|
+
required: false,
|
|
138
|
+
fields: {
|
|
139
|
+
outputMode: { type: 'string', enum: HOOK_OUTPUT_MODES },
|
|
140
|
+
},
|
|
141
|
+
},
|
|
94
142
|
roleSelection: {
|
|
95
143
|
type: 'object',
|
|
96
144
|
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, INGEST_ORCHESTRATION_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,8 @@ export async function ingestDocuments(inputPaths, {
|
|
|
129
166
|
sync = false,
|
|
130
167
|
strict = false,
|
|
131
168
|
highFidelity = true,
|
|
169
|
+
strategy = null,
|
|
170
|
+
orchestration = null,
|
|
132
171
|
env = process.env,
|
|
133
172
|
} = {}) {
|
|
134
173
|
if (!Array.isArray(inputPaths) || inputPaths.length === 0) {
|
|
@@ -143,12 +182,24 @@ export async function ingestDocuments(inputPaths, {
|
|
|
143
182
|
throw new Error('No supported document files found');
|
|
144
183
|
}
|
|
145
184
|
|
|
185
|
+
const { config } = loadProjectConfig(cwd, env);
|
|
186
|
+
const resolvedStrategy = resolveIngestStrategy({ config, env, override: strategy, orchestrationOverride: orchestration, cwd });
|
|
187
|
+
|
|
146
188
|
const results = [];
|
|
147
189
|
let totalDrops = 0;
|
|
190
|
+
let fallbackApplied = null;
|
|
148
191
|
for (const sourcePath of files) {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
:
|
|
192
|
+
const routed = await extractWithStrategy(sourcePath, {
|
|
193
|
+
strategy: resolvedStrategy.strategy,
|
|
194
|
+
fallback: resolvedStrategy.fallback,
|
|
195
|
+
model: resolvedStrategy.model,
|
|
196
|
+
provider: resolvedStrategy.provider,
|
|
197
|
+
highFidelity,
|
|
198
|
+
maxChars: 200_000,
|
|
199
|
+
env,
|
|
200
|
+
});
|
|
201
|
+
const extracted = routed.extracted;
|
|
202
|
+
if (routed.fallbackApplied) fallbackApplied = routed.fallbackApplied;
|
|
152
203
|
totalDrops += (extracted.droppedInfo ?? []).reduce((a, d) => a + (Number(d.count) || 1), 0);
|
|
153
204
|
const metadata = extractDocumentMetadata(sourcePath);
|
|
154
205
|
const targetPath = resolveOutputPath(sourcePath, { cwd, outputPath, outputDir, target });
|
|
@@ -213,6 +264,15 @@ export async function ingestDocuments(inputPaths, {
|
|
|
213
264
|
droppedInfo: results.flatMap((r) => r.droppedInfo.map((d) => ({ ...d, sourcePath: r.sourcePath }))),
|
|
214
265
|
droppedCount: totalDrops,
|
|
215
266
|
highFidelity,
|
|
267
|
+
ingestion: {
|
|
268
|
+
strategy: resolvedStrategy.strategy,
|
|
269
|
+
fallback: resolvedStrategy.fallback,
|
|
270
|
+
orchestration: resolvedStrategy.orchestration,
|
|
271
|
+
model: resolvedStrategy.model,
|
|
272
|
+
provider: resolvedStrategy.provider,
|
|
273
|
+
fallbackApplied,
|
|
274
|
+
execution: resolvedStrategy.execution,
|
|
275
|
+
},
|
|
216
276
|
};
|
|
217
277
|
}
|
|
218
278
|
|
|
@@ -224,26 +284,40 @@ export async function runIngestCli(argv = process.argv.slice(2), { cwd = process
|
|
|
224
284
|
let sync = false;
|
|
225
285
|
let strict = false;
|
|
226
286
|
let highFidelity = true;
|
|
287
|
+
let strategy = null;
|
|
288
|
+
let orchestration = null;
|
|
227
289
|
|
|
228
290
|
for (const arg of argv) {
|
|
229
291
|
if (arg.startsWith('--out=')) outputPath = arg.split('=').slice(1).join('=');
|
|
230
292
|
else if (arg.startsWith('--out-dir=')) outputDir = arg.split('=').slice(1).join('=');
|
|
231
293
|
else if (arg.startsWith('--target=')) target = arg.split('=').slice(1).join('=');
|
|
294
|
+
else if (arg.startsWith('--strategy=')) strategy = arg.split('=').slice(1).join('=');
|
|
295
|
+
else if (arg === '--strategy') strategy = '';
|
|
296
|
+
else if (arg.startsWith('--orchestration=')) orchestration = arg.split('=').slice(1).join('=');
|
|
232
297
|
else if (arg === '--sync') sync = true;
|
|
233
298
|
else if (arg === '--strict') strict = true;
|
|
234
299
|
else if (arg === '--legacy-extractor') highFidelity = false;
|
|
300
|
+
else if (strategy === '') strategy = arg;
|
|
235
301
|
else inputs.push(arg);
|
|
236
302
|
}
|
|
237
303
|
|
|
238
304
|
if (inputs.length === 0) {
|
|
239
305
|
throw new Error(
|
|
240
306
|
`Usage: construct ingest <file-or-dir> [more paths] [--out=FILE] [--out-dir=DIR] ` +
|
|
241
|
-
`[--target=sibling|knowledge/<subdir>] [--sync] [--strict] [--legacy-extractor]\n` +
|
|
307
|
+
`[--target=sibling|knowledge/<subdir>] [--strategy=adapter|provider] [--orchestration=prompt-only|orchestrated] [--sync] [--strict] [--legacy-extractor]\n` +
|
|
308
|
+
` --strategy: extraction strategy override (adapter = local extractors; provider = configured provider/model)\n` +
|
|
309
|
+
` --orchestration: prompt-only (deterministic extraction) or orchestrated (engage the specialist chain)\n` +
|
|
242
310
|
` --strict: exit non-zero if any extraction drops occur\n` +
|
|
243
311
|
` --legacy-extractor: use the pre-docling regex extractor (lower fidelity)\n` +
|
|
244
312
|
` knowledge subdirs: ${KNOWLEDGE_SUBDIRS.join(', ')}`,
|
|
245
313
|
);
|
|
246
314
|
}
|
|
315
|
+
if (strategy !== null && strategy !== '' && !INGEST_STRATEGIES.includes(strategy)) {
|
|
316
|
+
throw new Error(`Unsupported strategy: ${strategy}. Valid strategies: ${INGEST_STRATEGIES.join(', ')}`);
|
|
317
|
+
}
|
|
318
|
+
if (orchestration !== null && !INGEST_ORCHESTRATION_STRATEGIES.includes(orchestration)) {
|
|
319
|
+
throw new Error(`Unsupported orchestration: ${orchestration}. Valid values: ${INGEST_ORCHESTRATION_STRATEGIES.join(', ')}`);
|
|
320
|
+
}
|
|
247
321
|
const knowledgeTargets = KNOWLEDGE_SUBDIRS.map((s) => `knowledge/${s}`);
|
|
248
322
|
if (!['sibling', ...knowledgeTargets].includes(target)) {
|
|
249
323
|
throw new Error(
|
|
@@ -251,5 +325,5 @@ export async function runIngestCli(argv = process.argv.slice(2), { cwd = process
|
|
|
251
325
|
);
|
|
252
326
|
}
|
|
253
327
|
|
|
254
|
-
return ingestDocuments(inputs, { cwd, outputPath, outputDir, target, sync, strict, highFidelity, env });
|
|
328
|
+
return ingestDocuments(inputs, { cwd, outputPath, outputDir, target, sync, strict, highFidelity, strategy: strategy || null, orchestration: orchestration || null, env });
|
|
255
329
|
}
|
|
@@ -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
|
+
}
|