@inneranimalmedia/agentsam-sdk 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -15
- package/docs/CAPABILITIES.md +93 -0
- package/docs/DEPLOY_RECEIPTS.md +83 -0
- package/docs/RECON.md +165 -0
- package/docs/RELEASES.md +4 -3
- package/docs/portable-knowledge.md +4 -4
- package/docs/sdk-2.0-release.md +19 -21
- package/package.json +8 -1
- package/packages/identity/package.json +1 -1
- package/packages/identity/src/frontend/auth-portal/README.md +1 -1
- package/packages/identity/src/index.js +2 -0
- package/protocol/capabilities/capability-manifest.schema.json +36 -0
- package/protocol/capabilities/manifest.json +179 -0
- package/protocol/capabilities/repository-audit-input.schema.json +14 -0
- package/protocol/capabilities/repository-audit.schema.json +30 -0
- package/protocol/capabilities/repository-snapshot-input.schema.json +11 -0
- package/protocol/capabilities/repository-snapshot.schema.json +20 -0
- package/protocol/knowledge/chunk.schema.json +15 -73
- package/protocol/knowledge/document.schema.json +10 -48
- package/protocol/knowledge/index-config.schema.json +2 -2
- package/protocol/knowledge/repository.schema.json +11 -52
- package/protocol/knowledge/retrieval-query.schema.json +13 -63
- package/protocol/knowledge/source.schema.json +9 -43
- package/protocol/presets/catalog.json +48 -0
- package/protocol/recon/README.md +19 -0
- package/protocol/recon/finding-report.schema.json +46 -0
- package/protocol/recon/task-packet.schema.json +79 -0
- package/python/agentsam_sdk/knowledge/models.py +19 -7
- package/python/agentsam_sdk/repository/__main__.py +2 -2
- package/python/agentsam_sdk/repository/recon/__init__.py +28 -0
- package/python/agentsam_sdk/repository/recon/__main__.py +3 -0
- package/python/agentsam_sdk/repository/recon/cli.py +178 -0
- package/python/agentsam_sdk/repository/recon/packet.py +294 -0
- package/python/agentsam_sdk/repository/recon/validate.py +76 -0
- package/python/tests/test_knowledge_models.py +6 -2
- package/python/tests/test_recon.py +257 -0
- package/src/agent/capability-adapter.js +50 -0
- package/src/agent/index.js +2 -0
- package/src/agent/repository-audit.js +188 -0
- package/src/capabilities/index.js +7 -0
- package/src/capabilities/manifest.js +22 -0
- package/src/capabilities/repository-snapshot.js +180 -0
- package/src/cli.js +68 -8
- package/src/commands/deploy-receipt.js +129 -0
- package/src/commands/deploy.js +0 -1
- package/src/commands/knowledge.js +5 -6
- package/src/commands/product.js +119 -0
- package/src/commands/recon.js +71 -0
- package/src/index.js +17 -0
- package/src/knowledge/config.js +12 -6
- package/src/knowledge/contracts.js +1 -1
- package/src/knowledge/engine.js +1 -1
- package/src/knowledge/service/server.js +2 -2
- package/src/lib/deploy-receipt/index.js +246 -0
- package/src/lib/git-context.js +3 -1
- package/src/presets/index.js +20 -0
- package/src/repository/index.js +4 -0
- package/test/agent-capabilities.test.mjs +67 -0
- package/test/capabilities.test.mjs +84 -0
- package/test/deploy-receipt.test.mjs +91 -0
- package/test/portable-context.test.mjs +11 -1
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { captureDeployReceipt, finalizeDeployReceipt, showLatestDeployReceipt } from '../lib/deploy-receipt/index.js';
|
|
2
|
+
|
|
3
|
+
export function printDeployReceiptHelp() {
|
|
4
|
+
console.log(`
|
|
5
|
+
agentsam deploy-receipt — reusable Merkle deployment/checkpoint lifecycle
|
|
6
|
+
|
|
7
|
+
capture [path] Capture the current tree and compare to the last promoted baseline
|
|
8
|
+
success [path] Finalize a successful run and promote its snapshot to latest
|
|
9
|
+
failure [path] Finalize a failed run without advancing the baseline
|
|
10
|
+
show [path] Print the latest promoted receipt
|
|
11
|
+
|
|
12
|
+
--project <id> Logical project identifier (defaults to directory name)
|
|
13
|
+
--state-dir <path> Runtime state directory (default: .agentsam/deploy-merkle)
|
|
14
|
+
--baseline <snapshot> Explicit baseline snapshot for capture (e.g. restored from R2)
|
|
15
|
+
--baseline-source <label> Baseline provenance label (e.g. r2, local-cache)
|
|
16
|
+
--include <default-rule> Include one AgentSam default-ignored category
|
|
17
|
+
--exclude <path-or-name> Additional literal exclusion (repeatable)
|
|
18
|
+
--max-changed-files <n> Receipt path cap (default 100)
|
|
19
|
+
--deployment-id <id> Deployment/ledger identity for success/failure
|
|
20
|
+
--worker-version <id> Provider version identity for success/failure
|
|
21
|
+
--metadata-json <json> Extra compact receipt metadata object
|
|
22
|
+
--json Machine-readable output
|
|
23
|
+
|
|
24
|
+
Runtime state is intentionally not source. The state directory is excluded from the
|
|
25
|
+
captured Merkle tree, successful finalize advances latest.*, and failure never does.
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
agentsam deploy-receipt capture . --project my-worker --json
|
|
29
|
+
wrangler deploy
|
|
30
|
+
agentsam deploy-receipt success . --deployment-id dep_123 --json
|
|
31
|
+
agentsam deploy-receipt failure . --deployment-id dep_124 --json
|
|
32
|
+
`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseJsonObject(value, flag) {
|
|
36
|
+
let parsed;
|
|
37
|
+
try { parsed = JSON.parse(value); } catch { throw new Error(`${flag} must be valid JSON.`); }
|
|
38
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error(`${flag} must be a JSON object.`);
|
|
39
|
+
return parsed;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parse(argv) {
|
|
43
|
+
const [command, ...args] = argv;
|
|
44
|
+
const normalized = command === 'fail' ? 'failure' : command === 'promote' ? 'success' : command;
|
|
45
|
+
if (!['capture', 'success', 'failure', 'show'].includes(normalized)) throw new Error(`Unknown deploy-receipt command: ${command || ''}`);
|
|
46
|
+
const opts = { command: normalized, root: '.', include: [], exclude: [], json: false, metadata: {} };
|
|
47
|
+
let positional = false;
|
|
48
|
+
for (let i = 0; i < args.length; i++) {
|
|
49
|
+
const arg = args[i];
|
|
50
|
+
if (arg === '--') { positional = true; continue; }
|
|
51
|
+
if (positional || !arg.startsWith('-')) {
|
|
52
|
+
if (opts.root !== '.') throw new Error('Only one root path may be supplied.');
|
|
53
|
+
opts.root = arg;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (arg === '--json') { opts.json = true; continue; }
|
|
57
|
+
if (['--project', '--state-dir', '--baseline', '--baseline-source', '--deployment-id', '--worker-version', '--metadata-json', '--max-changed-files', '--include', '--exclude'].includes(arg)) {
|
|
58
|
+
const value = args[++i];
|
|
59
|
+
if (value == null || value.startsWith('--')) throw new Error(`Missing value for ${arg}`);
|
|
60
|
+
if (arg === '--include') opts.include.push(value);
|
|
61
|
+
else if (arg === '--exclude') opts.exclude.push(value);
|
|
62
|
+
else if (arg === '--metadata-json') opts.metadata = parseJsonObject(value, arg);
|
|
63
|
+
else if (arg === '--max-changed-files') {
|
|
64
|
+
const count = Number(value);
|
|
65
|
+
if (!Number.isSafeInteger(count) || count < 1 || count > 10000) throw new Error('--max-changed-files must be an integer from 1 to 10000.');
|
|
66
|
+
opts.maxChangedFiles = count;
|
|
67
|
+
} else opts[arg.slice(2).replaceAll('-', '_')] = value;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
71
|
+
}
|
|
72
|
+
if (opts.command !== 'capture' && (opts.baseline || opts.baseline_source || opts.include.length || opts.exclude.length || opts.maxChangedFiles)) {
|
|
73
|
+
throw new Error('Baseline/include/exclude/change-cap options apply only to capture.');
|
|
74
|
+
}
|
|
75
|
+
if (!['success', 'failure'].includes(opts.command) && (opts.deployment_id || opts.worker_version)) {
|
|
76
|
+
throw new Error('--deployment-id and --worker-version apply only to success/failure.');
|
|
77
|
+
}
|
|
78
|
+
return opts;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function human(result, command) {
|
|
82
|
+
if (!result) return 'No promoted deploy receipt.\n';
|
|
83
|
+
const receipt = result.receipt || result;
|
|
84
|
+
if (command === 'capture') {
|
|
85
|
+
const delta = receipt.diff_stats ? `${receipt.diff_stats.added} added, ${receipt.diff_stats.modified} modified, ${receipt.diff_stats.removed} removed` : 'no baseline';
|
|
86
|
+
return `Captured ${receipt.root_hash} (${delta}; baseline=${receipt.baseline_source}).\n`;
|
|
87
|
+
}
|
|
88
|
+
return `${receipt.status}: ${receipt.root_hash}${receipt.deployment_id ? ` deployment=${receipt.deployment_id}` : ''}\n`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function runDeployReceipt(argv = []) {
|
|
92
|
+
if (!argv.length || argv.includes('--help') || argv.includes('-h')) { printDeployReceiptHelp(); return; }
|
|
93
|
+
try {
|
|
94
|
+
const opts = parse(argv);
|
|
95
|
+
let result;
|
|
96
|
+
if (opts.command === 'capture') {
|
|
97
|
+
result = await captureDeployReceipt({
|
|
98
|
+
root: opts.root,
|
|
99
|
+
project: opts.project,
|
|
100
|
+
stateDir: opts.state_dir,
|
|
101
|
+
baselineSnapshot: opts.baseline,
|
|
102
|
+
baselineSource: opts.baseline_source,
|
|
103
|
+
include: opts.include,
|
|
104
|
+
exclude: opts.exclude,
|
|
105
|
+
maxChangedFiles: opts.maxChangedFiles,
|
|
106
|
+
metadata: opts.metadata,
|
|
107
|
+
});
|
|
108
|
+
} else if (opts.command === 'show') {
|
|
109
|
+
result = await showLatestDeployReceipt({ root: opts.root, stateDir: opts.state_dir });
|
|
110
|
+
} else {
|
|
111
|
+
result = await finalizeDeployReceipt({
|
|
112
|
+
root: opts.root,
|
|
113
|
+
stateDir: opts.state_dir,
|
|
114
|
+
status: opts.command === 'success' ? 'success' : 'failed',
|
|
115
|
+
deploymentId: opts.deployment_id,
|
|
116
|
+
workerVersionId: opts.worker_version,
|
|
117
|
+
metadata: opts.metadata,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
const output = result?.receipt || result;
|
|
121
|
+
if (opts.json) process.stdout.write(JSON.stringify(output, null, 2) + '\n');
|
|
122
|
+
else process.stdout.write(human(result, opts.command));
|
|
123
|
+
return result;
|
|
124
|
+
} catch (error) {
|
|
125
|
+
process.exitCode = 2;
|
|
126
|
+
const json = argv.includes('--json');
|
|
127
|
+
process.stderr.write(json ? JSON.stringify({ error: error.message }) + '\n' : `Deploy receipt: ${error.message}\n`);
|
|
128
|
+
}
|
|
129
|
+
}
|
package/src/commands/deploy.js
CHANGED
|
@@ -24,12 +24,12 @@ async function openStore(root, config, readOnly = false) {
|
|
|
24
24
|
function provider() { return createGeminiEmbedder({ apiKey: process.env.GEMINI_API_KEY }); }
|
|
25
25
|
|
|
26
26
|
export async function runRepositoryInit(argv) {
|
|
27
|
-
const { values: opts, positionals } = flags(argv, { existing: { type: 'boolean' }, yes: { type: 'boolean', short: 'y' }, include: { type: 'string' }, exclude: { type: 'string' }, scope: { type: 'string' },
|
|
28
|
-
if (opts.help) { console.log('agentsam init [.] [--cwd PATH] [--yes] [--include src,docs] [--exclude src/generated] [--scope NAME] [--target local|production] [--
|
|
27
|
+
const { values: opts, positionals } = flags(argv, { existing: { type: 'boolean' }, yes: { type: 'boolean', short: 'y' }, include: { type: 'string' }, exclude: { type: 'string' }, scope: { type: 'string' }, target: { type: 'string' }, dimensions: { type: 'string' } });
|
|
28
|
+
if (opts.help) { console.log('agentsam init [.] [--cwd PATH] [--yes] [--include src,docs] [--exclude src/generated] [--scope NAME] [--target local|production] [--dimensions 768]'); return; }
|
|
29
29
|
if (positionals.length > 1 || (positionals[0] && positionals[0] !== '.')) throw new Error('Use init . --cwd PATH to adopt an existing repository, or init --name NAME to scaffold.');
|
|
30
30
|
const root = repositoryRoot(opts.cwd);
|
|
31
31
|
if (fs.existsSync(path.join(root, CONFIG_PATH))) throw new Error(`${CONFIG_PATH} already exists; edit it to change scope/profile. Existing configuration was preserved.`);
|
|
32
|
-
let include = opts.include, exclude = opts.exclude, target = opts.target,
|
|
32
|
+
let include = opts.include, exclude = opts.exclude, target = opts.target, dimensions = opts.dimensions;
|
|
33
33
|
if (!opts.yes) {
|
|
34
34
|
if (!process.stdin.isTTY) throw new Error('Existing-repository setup needs a terminal or --yes with explicit options.');
|
|
35
35
|
const prompt = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -38,11 +38,10 @@ export async function runRepositoryInit(argv) {
|
|
|
38
38
|
include ??= await prompt.question('1) Include files/directories, comma-separated [.]: ') || '.';
|
|
39
39
|
exclude ??= await prompt.question('2) Exclude files/directories [none]: ') || '';
|
|
40
40
|
target ??= await prompt.question('3) Storage: local or production [local]: ') || 'local';
|
|
41
|
-
|
|
42
|
-
dimensions ??= await prompt.question('5) Gemini Embedding 2 dimensions [768]: ') || '768';
|
|
41
|
+
dimensions ??= await prompt.question('4) Gemini Embedding 2 dimensions [768]: ') || '768';
|
|
43
42
|
} finally { prompt.close(); }
|
|
44
43
|
}
|
|
45
|
-
const config = initRepository(root, { include: split(include || '.'), exclude: split(exclude || ''), scope: opts.scope || 'default', target: target || 'local',
|
|
44
|
+
const config = initRepository(root, { include: split(include || '.'), exclude: split(exclude || ''), scope: opts.scope || 'default', target: target || 'local', dimensions: Number(dimensions || 768) });
|
|
46
45
|
show({ root, config: CONFIG_PATH, storage: config.storage.driver, next: config.storage.driver === 'postgres' ? ['agentsam index setup-store', 'agentsam index plan', 'agentsam index run'] : ['agentsam index plan', 'agentsam index run', 'agentsam search "your symbol"'], note: 'No indexing, credentials, network calls, or source-file changes during setup.' });
|
|
47
46
|
}
|
|
48
47
|
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { getCapability, getCapabilityManifest, listCapabilities, repositorySnapshot } from '../capabilities/index.js';
|
|
5
|
+
import { getAddon, listAddons, listPresets } from '../presets/index.js';
|
|
6
|
+
|
|
7
|
+
function parseCommon(argv = []) {
|
|
8
|
+
const out = { json: false, cwd: process.cwd(), positionals: [] };
|
|
9
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
10
|
+
const arg = argv[i];
|
|
11
|
+
if (arg === '--json') out.json = true;
|
|
12
|
+
else if (arg === '--cwd') out.cwd = argv[++i] || out.cwd;
|
|
13
|
+
else out.positionals.push(arg);
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function runInspect(argv = []) {
|
|
19
|
+
const opts = parseCommon(argv);
|
|
20
|
+
let churnDays = 30;
|
|
21
|
+
for (let i = 0; i < opts.positionals.length; i += 1) {
|
|
22
|
+
if (opts.positionals[i] === '--churn-days') churnDays = Number(opts.positionals[++i] || 30);
|
|
23
|
+
else if (opts.positionals[i] === 'repository') continue;
|
|
24
|
+
else throw new Error(`unknown inspect option: ${opts.positionals[i]}`);
|
|
25
|
+
}
|
|
26
|
+
const result = await repositorySnapshot({ cwd: opts.cwd, churnDays });
|
|
27
|
+
if (opts.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
28
|
+
else {
|
|
29
|
+
console.log(`\nRepository snapshot ${result.snapshot_id}`);
|
|
30
|
+
console.log(` repo ${result.repository.full_name || result.repository.repository_id || '(local)'}`);
|
|
31
|
+
console.log(` revision ${result.repository.revision_sha}`);
|
|
32
|
+
console.log(` merkle ${result.tree.merkle_root}`);
|
|
33
|
+
console.log(` files ${result.intelligence.summary?.file_count ?? result.tree.stats?.files ?? 'unknown'}`);
|
|
34
|
+
console.log(` knowledge ${result.knowledge?.indexed ? result.knowledge.generation_id : result.knowledge?.configured ? 'configured / not indexed' : 'not configured'}`);
|
|
35
|
+
console.log(` deploy ${result.deploy?.status || 'no trusted receipt'}`);
|
|
36
|
+
console.log(` content ${result.content_hash}\n`);
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function runCapabilities(argv = []) {
|
|
42
|
+
const opts = parseCommon(argv);
|
|
43
|
+
const id = opts.positionals[0] || '';
|
|
44
|
+
const result = id ? getCapability(id) : getCapabilityManifest();
|
|
45
|
+
if (id && !result) throw new Error(`unknown_capability:${id}`);
|
|
46
|
+
if (opts.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
47
|
+
else if (id) console.log(`${result.id}\n ${result.description}\n cli: ${result.cli || '(library only)'}\n model required: ${result.model_required ? 'yes' : 'no'}`);
|
|
48
|
+
else {
|
|
49
|
+
console.log('\nAgentSam deterministic capabilities\n');
|
|
50
|
+
for (const row of listCapabilities()) console.log(` ${row.id.padEnd(26)} ${row.description}`);
|
|
51
|
+
console.log('');
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function projectConfigPath(cwd) { return path.join(cwd, '.agentsam', 'config.json'); }
|
|
57
|
+
function featureStatePath(cwd) { return path.join(cwd, '.agentsam', 'features.json'); }
|
|
58
|
+
|
|
59
|
+
export function applyPresetSelection(cwd, preset) {
|
|
60
|
+
const filename = projectConfigPath(cwd);
|
|
61
|
+
if (!fs.existsSync(filename)) throw new Error('not_agentsam_project');
|
|
62
|
+
const config = JSON.parse(fs.readFileSync(filename, 'utf8'));
|
|
63
|
+
const next = { ...config, preset: preset.id, features: [...preset.features], capabilities: [...preset.capabilities] };
|
|
64
|
+
fs.writeFileSync(filename, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
|
|
65
|
+
return next;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function runAdd(argv = []) {
|
|
69
|
+
const opts = parseCommon(argv);
|
|
70
|
+
const id = opts.positionals[0];
|
|
71
|
+
if (!id || id === '--help') {
|
|
72
|
+
const rows = listAddons();
|
|
73
|
+
console.log(`agentsam add <${rows.map(x => x.id).join('|')}> [--cwd PATH] [--json]`);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const addon = getAddon(id);
|
|
77
|
+
if (!addon) throw new Error(`unknown_addon:${id}`);
|
|
78
|
+
const cwd = path.resolve(opts.cwd);
|
|
79
|
+
if (!fs.existsSync(projectConfigPath(cwd))) throw new Error('not_agentsam_project');
|
|
80
|
+
fs.mkdirSync(path.dirname(featureStatePath(cwd)), { recursive: true });
|
|
81
|
+
let state = { schema_version: 1, features: {} };
|
|
82
|
+
if (fs.existsSync(featureStatePath(cwd))) state = JSON.parse(fs.readFileSync(featureStatePath(cwd), 'utf8'));
|
|
83
|
+
state.features ||= {};
|
|
84
|
+
state.features[addon.id] = { selected: true, capabilities: addon.capabilities, selected_at: new Date().toISOString() };
|
|
85
|
+
fs.writeFileSync(featureStatePath(cwd), `${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
|
86
|
+
if (addon.id === 'deploy-cloudflare') {
|
|
87
|
+
const config = JSON.parse(fs.readFileSync(projectConfigPath(cwd), 'utf8'));
|
|
88
|
+
config.deploy_target = 'cloudflare';
|
|
89
|
+
fs.writeFileSync(projectConfigPath(cwd), `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
90
|
+
}
|
|
91
|
+
const result = { ok: true, feature: addon.id, capabilities: addon.capabilities, description: addon.description, state_file: '.agentsam/features.json' };
|
|
92
|
+
if (opts.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
93
|
+
else console.log(`\nAdded ${addon.id}\n ${addon.description}\n state: ${result.state_file}\n`);
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function runDev(argv = []) {
|
|
98
|
+
const opts = parseCommon(argv);
|
|
99
|
+
if (opts.positionals.length) throw new Error(`unknown dev option: ${opts.positionals[0]}`);
|
|
100
|
+
const cwd = path.resolve(opts.cwd);
|
|
101
|
+
const pkgFile = path.join(cwd, 'package.json');
|
|
102
|
+
if (!fs.existsSync(pkgFile)) throw new Error('package_json_not_found');
|
|
103
|
+
const pkg = JSON.parse(fs.readFileSync(pkgFile, 'utf8'));
|
|
104
|
+
if (!pkg.scripts?.dev) throw new Error('dev_script_not_found');
|
|
105
|
+
if (/\bagentsam\s+dev\b/.test(pkg.scripts.dev)) throw new Error('recursive_agentsam_dev_script');
|
|
106
|
+
const child = spawn(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'dev'], { cwd, stdio: 'inherit', env: process.env });
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
child.once('error', reject);
|
|
109
|
+
child.once('exit', (code, signal) => {
|
|
110
|
+
if (signal) return reject(new Error(`dev_process_signal:${signal}`));
|
|
111
|
+
process.exitCode = code || 0;
|
|
112
|
+
resolve(code || 0);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function printProductCatalog() {
|
|
118
|
+
return { presets: listPresets(), addons: listAddons(), capabilities: listCapabilities() };
|
|
119
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const PYTHON_ROOT = fileURLToPath(new URL('../../python', import.meta.url));
|
|
6
|
+
|
|
7
|
+
export function printReconHelp() {
|
|
8
|
+
console.log(`
|
|
9
|
+
agentsam recon — bounded-worker task packets and finding-report validation
|
|
10
|
+
|
|
11
|
+
pack --repo-root <path> --question "..." --slice <path[:start-end]> [--slice ...]
|
|
12
|
+
Build a bounded ReconTaskPacket (<=5 slices, hard ceilings)
|
|
13
|
+
validate --packet <file> --report <file>
|
|
14
|
+
Validate a ReconFindingReport against its packet
|
|
15
|
+
|
|
16
|
+
pack options:
|
|
17
|
+
--repo-root <path> Default: .
|
|
18
|
+
--question <text> Required. Exactly one bounded question.
|
|
19
|
+
--slice <path[:start-end]> Repeatable, up to 5. e.g. --slice src/foo.js:10-40
|
|
20
|
+
--task-id <id> Default: generated
|
|
21
|
+
--out <file> Write the packet JSON to a file instead of stdout
|
|
22
|
+
|
|
23
|
+
validate options:
|
|
24
|
+
--packet <file> Required. Path to a packet JSON file.
|
|
25
|
+
--report <file> Required. Path to a worker's report JSON file.
|
|
26
|
+
|
|
27
|
+
Neither command calls a model or writes into the target repository. This is a thin
|
|
28
|
+
passthrough to the bundled Python module (agentsam_sdk.repository.recon) — same
|
|
29
|
+
engine as \`python -m agentsam_sdk.repository.recon\`. See docs/RECON.md.
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
agentsam recon pack --repo-root . --question "Does this still read workspace_id?" \\
|
|
33
|
+
--slice backend/workflows/repository.js:1-180 --out /tmp/packet.json
|
|
34
|
+
agentsam recon validate --packet /tmp/packet.json --report /tmp/report.json
|
|
35
|
+
|
|
36
|
+
For a raw rg/ast-grep hit list instead of hand-picked slices, use
|
|
37
|
+
agentsam_sdk.repository.recon.{from_ripgrep,from_ast_grep,from_matches}() from
|
|
38
|
+
Python directly — those have no Node CLI surface yet since hit shapes vary by tool.
|
|
39
|
+
`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function run(command, args) {
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
const child = spawn(command, args, {
|
|
45
|
+
stdio: 'inherit',
|
|
46
|
+
cwd: process.cwd(),
|
|
47
|
+
env: { ...process.env, PYTHONPATH: [PYTHON_ROOT, process.env.PYTHONPATH].filter(Boolean).join(path.delimiter) },
|
|
48
|
+
});
|
|
49
|
+
child.once('error', (err) => {
|
|
50
|
+
console.error(`\n ✗ Could not run Python (${err.message}). Python 3.10+ must be on PATH for \`agentsam recon\`.\n`);
|
|
51
|
+
resolve(1);
|
|
52
|
+
});
|
|
53
|
+
child.once('exit', (code, signal) => {
|
|
54
|
+
if (signal) { console.error(`\n ✗ recon ${signal}\n`); resolve(1); }
|
|
55
|
+
else resolve(code ?? 1);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function runRecon(argv = []) {
|
|
61
|
+
if (!argv.length || argv.includes('--help') || argv.includes('-h')) { printReconHelp(); return; }
|
|
62
|
+
const [command] = argv;
|
|
63
|
+
if (!['pack', 'validate'].includes(command)) {
|
|
64
|
+
console.error(`\n ✗ Unknown recon command: ${command}. Use \`agentsam recon --help\`.\n`);
|
|
65
|
+
process.exitCode = 2;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const python = process.platform === 'win32' ? 'python' : 'python3';
|
|
69
|
+
const code = await run(python, ['-B', '-m', 'agentsam_sdk.repository.recon', ...argv]);
|
|
70
|
+
if (code !== 0) process.exitCode = code;
|
|
71
|
+
}
|
package/src/index.js
CHANGED
|
@@ -6,6 +6,14 @@ export { AgentSam } from './AgentSam.js';
|
|
|
6
6
|
export { routeIntent } from './lib/router.js';
|
|
7
7
|
export { getToolCatalog } from './lib/tools.js';
|
|
8
8
|
export { scaffoldProject } from './lib/scaffold.js';
|
|
9
|
+
export {
|
|
10
|
+
DEFAULT_DEPLOY_EXCLUDES,
|
|
11
|
+
captureDeployReceipt,
|
|
12
|
+
finalizeDeployReceipt,
|
|
13
|
+
showLatestDeployReceipt,
|
|
14
|
+
captureCheckpoint,
|
|
15
|
+
promoteCheckpoint,
|
|
16
|
+
} from './lib/deploy-receipt/index.js';
|
|
9
17
|
export {
|
|
10
18
|
normalizeGitRemote,
|
|
11
19
|
resolveGitContext,
|
|
@@ -23,12 +31,21 @@ export {
|
|
|
23
31
|
SHELL_PHASES,
|
|
24
32
|
listSlashCommands,
|
|
25
33
|
} from './lib/slash-commands.js';
|
|
34
|
+
export {
|
|
35
|
+
CAPABILITY_MANIFEST_VERSION,
|
|
36
|
+
getCapability,
|
|
37
|
+
getCapabilityManifest,
|
|
38
|
+
listCapabilities,
|
|
39
|
+
repositorySnapshot,
|
|
40
|
+
} from './capabilities/index.js';
|
|
41
|
+
export { getPreset, listPresets, resolvePreset, getAddon, listAddons } from './presets/index.js';
|
|
26
42
|
|
|
27
43
|
export {
|
|
28
44
|
createIdentityClient,
|
|
29
45
|
createIdentity,
|
|
30
46
|
GoogleProvider,
|
|
31
47
|
GithubProvider,
|
|
48
|
+
IamProvider,
|
|
32
49
|
GcpProvider,
|
|
33
50
|
EmailProvider,
|
|
34
51
|
getIdentityProvider,
|
package/src/knowledge/config.js
CHANGED
|
@@ -37,7 +37,8 @@ export function validateConfig(input) {
|
|
|
37
37
|
allowed(c.embedding, ['provider', 'model', 'revision', 'dimensions', 'parameters'], 'embedding');
|
|
38
38
|
allowed(c.storage, ['driver', 'connection_env'], 'storage');
|
|
39
39
|
if (c.version !== 1) throw new Error('Unsupported knowledge configuration version.');
|
|
40
|
-
|
|
40
|
+
if (typeof c.repository_id !== 'string' || !c.repository_id.trim()) throw new Error('repository_id is required.');
|
|
41
|
+
if (c.workspace_id != null && (typeof c.workspace_id !== 'string' || !c.workspace_id.trim())) throw new Error('workspace_id must be a non-empty string when present.');
|
|
41
42
|
if (typeof c.scope?.name !== 'string' || !c.scope.name.trim() || !Array.isArray(c.scope.include) || !c.scope.include.length) throw new Error('A named scope with at least one include path is required.');
|
|
42
43
|
c.scope.include = [...new Set(c.scope.include.map(relativeSelection))].sort();
|
|
43
44
|
c.scope.exclude = [...new Set((c.scope.exclude || []).map(relativeSelection))].sort();
|
|
@@ -49,10 +50,9 @@ export function validateConfig(input) {
|
|
|
49
50
|
if (c.storage.driver === 'postgres' && !/^[A-Z_][A-Z0-9_]*$/.test(c.storage.connection_env || '')) throw new Error('Postgres requires a connection_env name, never a connection string in config.');
|
|
50
51
|
return c;
|
|
51
52
|
}
|
|
52
|
-
export function defaultConfig({ include = ['.'], exclude = [], scope = 'default',
|
|
53
|
+
export function defaultConfig({ include = ['.'], exclude = [], scope = 'default', target = 'local', dimensions = 768 } = {}) {
|
|
53
54
|
if (!['local', 'production'].includes(target)) throw new Error('target must be local or production.');
|
|
54
|
-
|
|
55
|
-
return validateConfig({ version: 1, repository_id: randomUUID(), workspace_id: workspace,
|
|
55
|
+
return validateConfig({ version: 1, repository_id: randomUUID(),
|
|
56
56
|
scope: { name: scope, include, exclude }, chunking: { max_chars: 4000 },
|
|
57
57
|
embedding: { provider: 'gemini', model: 'gemini-embedding-2', revision: '1', dimensions, parameters: { task: 'code retrieval' } },
|
|
58
58
|
storage: target === 'local' ? { driver: 'sqlite' } : { driver: 'postgres', connection_env: 'AGENTSAM_DATABASE_URL' } });
|
|
@@ -65,5 +65,11 @@ export function initRepository(root, options = {}) {
|
|
|
65
65
|
fs.writeFileSync(target, JSON.stringify(config, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
|
|
66
66
|
return config;
|
|
67
67
|
}
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
// New portable configs are repository-scoped. Legacy configs that still carry
|
|
69
|
+
// workspace_id retain their original namespace so existing local generations stay readable.
|
|
70
|
+
export const scopeKey = config => config.workspace_id
|
|
71
|
+
? fingerprint([config.workspace_id, config.repository_id, config.scope.name])
|
|
72
|
+
: fingerprint([config.repository_id, config.scope.name]);
|
|
73
|
+
export const cacheNamespace = config => config.workspace_id
|
|
74
|
+
? fingerprint([config.workspace_id, config.repository_id])
|
|
75
|
+
: fingerprint([config.repository_id]);
|
|
@@ -8,7 +8,7 @@ export const KNOWLEDGE_OPERATIONS = Object.freeze({
|
|
|
8
8
|
export function assertRetrievalQuery(value) {
|
|
9
9
|
if (!value || typeof value !== "object") throw new TypeError("RetrievalQuery must be an object");
|
|
10
10
|
if (!String(value.text || "").trim()) throw new TypeError("RetrievalQuery.text is required");
|
|
11
|
-
if (!String(value.workspace_id
|
|
11
|
+
if (value.workspace_id != null && !String(value.workspace_id).trim()) throw new TypeError("RetrievalQuery.workspace_id must be non-empty when present");
|
|
12
12
|
const topK = value.top_k ?? 12;
|
|
13
13
|
if (!Number.isInteger(topK) || topK < 1 || topK > 100) throw new RangeError("top_k must be 1..100");
|
|
14
14
|
if (!Number.isInteger(value.token_budget ?? 8000) || (value.token_budget ?? 8000) < 256) throw new RangeError("token_budget must be at least 256");
|
package/src/knowledge/engine.js
CHANGED
|
@@ -113,6 +113,6 @@ export async function retrieve({ store, config: input, text, semantic = false, e
|
|
|
113
113
|
metadata: { generation_id: generation.id, symbol: hit.symbol, content_hash: hit.content_hash, freshness: 'working tree not checked' } }); estimatedTokens += tokens;
|
|
114
114
|
if (hits.length === topK) break;
|
|
115
115
|
}
|
|
116
|
-
return createContextPack({ queryId: randomUUID(), query: { text,
|
|
116
|
+
return createContextPack({ queryId: randomUUID(), query: { text, top_k: topK, token_budget: tokenBudget }, hits,
|
|
117
117
|
diagnostics: { generation_id: generation.id, source_hash: generation.source_hash, scope: generation.config.scope, mode: semantic ? 'semantic-exact' : 'lexical', freshness: 'snapshot; working tree not checked' } });
|
|
118
118
|
}
|
|
@@ -100,7 +100,7 @@ export async function startKnowledgeService({ stateDir, repositories, token, por
|
|
|
100
100
|
});
|
|
101
101
|
// Re-resolve registry on replay. Removed repositories do not get resumed.
|
|
102
102
|
const payload = JSON.parse(row.payload), registered = repositories[payload.request.repository];
|
|
103
|
-
if (!registered || registered.config.repository_id !== payload.config.repository_id ||
|
|
103
|
+
if (!registered || registered.config.repository_id !== payload.config.repository_id || fingerprint(registered.config.scope) !== payload.registered_scope || (!allowEmbeddings && ((payload.request.embed && payload.request.operation !== 'plan') || payload.request.semantic))) {
|
|
104
104
|
response = { ok: false, error: 'Repository registration changed; resubmit this job.' }; child.kill(); return;
|
|
105
105
|
}
|
|
106
106
|
child.send({ ...payload, root: registered.root, filename: path.join(stateDir, 'knowledge.sqlite'), maxFiles });
|
|
@@ -124,7 +124,7 @@ export async function startKnowledgeService({ stateDir, repositories, token, por
|
|
|
124
124
|
payload.registered_scope = fingerprint(repositories[payload.request.repository].config.scope);
|
|
125
125
|
const key = req.headers['idempotency-key'];
|
|
126
126
|
if (key !== undefined && (typeof key !== 'string' || !/^[\w:.-]{1,128}$/.test(key))) throw fail(400, 'Invalid Idempotency-Key.');
|
|
127
|
-
const digest = fingerprint(payload), idem = key ? fingerprint([payload.config.
|
|
127
|
+
const digest = fingerprint(payload), idem = key ? fingerprint([payload.config.repository_id, payload.registered_scope, key]) : null;
|
|
128
128
|
const previous = idem && db.prepare('SELECT * FROM jobs WHERE idem=?').get(idem);
|
|
129
129
|
if (previous) {
|
|
130
130
|
if (previous.digest !== digest) throw fail(409, 'Idempotency-Key already used for a different job.');
|