@inneranimalmedia/agentsam-sdk 2.4.1 → 2.6.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/AGENTSAM.md +55 -0
- package/README.md +17 -12
- package/bin/agentsam +2 -0
- package/docs/AGENTSAM_ASTRA_OPENAI_INTEGRATION.md +1363 -0
- package/docs/AGENTSAM_WORKBENCH.md +30 -0
- package/docs/AUTH_IDENTITY_CONTRACT.md +52 -0
- package/docs/CAPABILITIES.md +5 -3
- package/docs/CLI_SHELL.md +167 -80
- package/docs/CMS_STUDIO.md +101 -0
- package/docs/CONTEXT.md +170 -0
- package/docs/LOCAL_OLLAMA.md +58 -0
- package/docs/MERKLE.md +19 -19
- package/docs/PORTABLE_CONTEXT.md +4 -3
- package/docs/PROJECT_CONFIG.md +72 -0
- package/docs/RELEASES.md +35 -2
- package/docs/REPOSITORY_INTELLIGENCE.md +1 -1
- package/docs/REPOSITORY_KNOWLEDGE.md +114 -0
- package/docs/SDK_WORKER.md +86 -0
- package/docs/SECURITY.md +59 -22
- package/docs/client-cms-editor.md +15 -0
- package/docs/local-studio/WORKMODE_DONOR_NOTES.md +485 -0
- package/package.json +35 -10
- package/packages/connectors/cloudflare/package.json +10 -0
- package/packages/connectors/cloudflare/src/index.js +127 -0
- package/packages/connectors/cloudflare/src/owner.js +76 -0
- package/packages/connectors/cloudflare/src/routes.js +223 -0
- package/packages/connectors/cloudflare/src/vault.js +80 -0
- package/packages/connectors/cloudflare/tests/connector.test.mjs +44 -0
- package/packages/identity/package.json +10 -1
- package/packages/identity/src/contracts/auth-config.js +109 -0
- package/packages/identity/src/index.js +1 -0
- package/packages/identity/src/oauth/README.md +2 -2
- package/packages/identity/src/oauth/credentials.js +11 -4
- package/packages/identity/src/oauth/iam-platform.js +3 -3
- package/packages/identity/src/providers/iam/index.js +7 -7
- package/packages/identity/src/providers/iam/oauth.js +6 -4
- package/packages/identity/src/providers/iam/profile.js +5 -5
- package/packages/identity/tests/auth-config.test.mjs +61 -0
- package/packages/identity/tests/oauth-credentials.test.mjs +13 -2
- package/protocol/FILEMETA_V1.md +95 -0
- package/protocol/INSPECT_VIEWS_V1.md +40 -0
- package/protocol/MERKLE_PERSISTENCE_V1.md +50 -0
- package/protocol/MERKLE_V1.md +3 -1
- package/protocol/README.md +1 -0
- package/protocol/capabilities/cloudflare-cpu-audit-input.schema.json +19 -0
- package/protocol/capabilities/cloudflare-cpu-profile-input.schema.json +13 -0
- package/protocol/capabilities/cloudflare-wrangler-native-input.schema.json +19 -0
- package/protocol/capabilities/manifest.json +79 -3
- package/protocol/capabilities/repository-snapshot.schema.json +1 -0
- package/protocol/context/context-budget.schema.json +36 -0
- package/protocol/context/context-item.schema.json +19 -0
- package/protocol/context/resolved-context-pack.schema.json +47 -0
- package/protocol/context/result-policy.schema.json +17 -0
- package/protocol/knowledge/context-pack.schema.json +33 -0
- package/protocol/knowledge/retrieval-query.schema.json +68 -13
- package/protocol/models/README.md +373 -0
- package/protocol/models/model-inventory-v2.schema.json +212 -0
- package/python/README.md +15 -7
- package/python/agentsam_sdk/cli.py +0 -21
- package/python/agentsam_sdk/tui/README.md +17 -12
- package/python/agentsam_sdk/tui/bootstrap.py +2 -2
- package/python/agentsam_sdk/tui/demo.py +25 -10
- package/python/agentsam_sdk/tui/onboarding.py +208 -0
- package/python/tests/test_tui_cli.py +7 -6
- package/skills/README.md +21 -0
- package/skills/agentsam-app-fundamentals/SKILL.md +165 -0
- package/skills/agentsam-app-fundamentals/references/graphs-contracts-ast-merkle.md +89 -0
- package/skills/agentsam-app-fundamentals/references/trust-credentials-and-destinations.md +99 -0
- package/skills/agentsam-cloudflare-workers/SKILL.md +53 -0
- package/skills/agentsam-cloudflare-workers/references/cpu-profiling.md +16 -0
- package/skills/agentsam-cloudflare-workers/references/errors-and-observability.md +29 -0
- package/skills/agentsam-cloudflare-workers/references/wrangler-native-map.md +28 -0
- package/skills/agentsam-jr-dev/SKILL.md +232 -0
- package/skills/agentsam-jr-dev/references/real-application-logic.md +156 -0
- package/skills/agentsam-jr-dev/references/web-application-fundamentals.md +240 -0
- package/skills/agentsam-progression-guard/SKILL.md +197 -0
- package/skills/agentsam-progression-guard/references/checkpoint-chain.md +111 -0
- package/skills/agentsam-progression-guard/references/hooks-operational-io.md +96 -0
- package/skills/catalog.json +71 -0
- package/src/agent/capability-adapter.js +25 -13
- package/src/agent/index.js +1 -0
- package/src/agent/responses-runner.js +325 -0
- package/src/capabilities/index.js +7 -0
- package/src/capabilities/repository-snapshot-view.js +238 -0
- package/src/capabilities/repository-snapshot.js +28 -14
- package/src/cli.js +198 -76
- package/src/cloudflare/cpu-profile.js +115 -0
- package/src/cloudflare/index.js +14 -0
- package/src/cloudflare/wrangler.js +132 -0
- package/src/commands/account-auth.js +47 -0
- package/src/commands/cloudflare.js +58 -0
- package/src/commands/connections.js +93 -0
- package/src/commands/context-economics.js +114 -0
- package/src/commands/context.js +14 -2
- package/src/commands/db.js +4 -7
- package/src/commands/deploy.js +54 -32
- package/src/commands/eval.js +63 -0
- package/src/commands/interactive.js +18 -0
- package/src/commands/knowledge.js +27 -4
- package/src/commands/merkle-persist.js +118 -0
- package/src/commands/merkle.js +32 -17
- package/src/commands/models.js +152 -0
- package/src/commands/ollama.js +259 -0
- package/src/commands/preferences.js +144 -0
- package/src/commands/product.js +86 -16
- package/src/commands/resume.js +67 -0
- package/src/commands/security.js +8 -6
- package/src/commands/shell.js +405 -100
- package/src/commands/skills.js +66 -0
- package/src/commands/start-local.js +1 -1
- package/src/commands/tunnel.js +5 -4
- package/src/commands/whoami.js +86 -0
- package/src/context/budget.js +120 -0
- package/src/context/compact.js +28 -0
- package/src/context/index.js +7 -0
- package/src/context/rehydrate.js +35 -0
- package/src/context/resolve.js +116 -0
- package/src/context/result-policy.js +66 -0
- package/src/errors/diagnostic.js +160 -0
- package/src/errors/index.js +9 -0
- package/src/eval/context.js +191 -0
- package/src/eval/index.js +1 -0
- package/src/index.js +70 -0
- package/src/indexing/execution-boundary.js +144 -0
- package/src/indexing/index.js +8 -0
- package/src/indexing/provider.js +41 -0
- package/src/knowledge/config.js +2 -2
- package/src/knowledge/context-pack.js +12 -1
- package/src/knowledge/contracts.js +9 -4
- package/src/knowledge/engine.js +7 -3
- package/src/knowledge/service/server.js +1 -1
- package/src/lib/account-session.js +98 -0
- package/src/lib/agent-instructions.js +73 -0
- package/src/lib/auth.js +4 -0
- package/src/lib/bridge-client.js +7 -5
- package/src/lib/cli-preferences.js +78 -0
- package/src/lib/core-client.js +8 -8
- package/src/lib/deploy/git-guard.js +69 -0
- package/src/lib/deploy/health.js +57 -0
- package/src/lib/deploy/local-studio.js +283 -0
- package/src/lib/deploy/secret-scan.js +65 -0
- package/src/lib/deploy-receipt/index.js +5 -2
- package/src/lib/detect-context.js +6 -5
- package/src/lib/execution-approvals.js +59 -0
- package/src/lib/identity-scaffold.js +1 -1
- package/src/lib/local-scaffold.js +35 -33
- package/src/lib/local-sessions.js +127 -0
- package/src/lib/local-status.js +9 -17
- package/src/lib/merkle/cloudflare-persistence.js +321 -0
- package/src/lib/merkle/filemeta.js +43 -0
- package/src/lib/merkle/git-ignore.js +24 -0
- package/src/lib/merkle/hash.js +1 -0
- package/src/lib/merkle/index.js +22 -0
- package/src/lib/merkle/persistence.js +72 -0
- package/src/lib/merkle/semantic.js +359 -0
- package/src/lib/merkle/snapshot.js +9 -3
- package/src/lib/merkle/tree.js +11 -6
- package/src/lib/project-config.js +227 -0
- package/src/lib/project-rules.js +68 -0
- package/src/lib/provider-credentials.js +83 -0
- package/src/lib/save-sdk-token.js +1 -1
- package/src/lib/scaffold/templates/worker-api/index.js +101 -20
- package/src/lib/scaffold/wizards/worker-api.js +27 -11
- package/src/lib/slash-commands.js +22 -15
- package/src/lib/tools.js +11 -5
- package/src/models/catalog.js +135 -0
- package/src/models/index.js +7 -0
- package/src/providers/index.js +5 -0
- package/src/providers/openai-responses.js +275 -0
- package/src/security/index.js +1 -0
- package/src/security/inventory.js +4 -1
- package/src/security/process.js +35 -9
- package/src/security/render.js +27 -5
- package/src/security/scan.js +24 -9
- package/src/security/trust-boundary.js +24 -0
- package/src/skills/index.js +64 -0
- package/src/telemetry/contracts.js +203 -0
- package/src/telemetry/events.js +48 -0
- package/src/telemetry/index.js +8 -0
- package/src/tools/hydrate.js +35 -0
- package/src/tools/index.js +2 -0
- package/src/tools/search.js +70 -0
- package/src/ui/ansi.js +1 -1
- package/src/ui/boot.js +54 -0
- package/src/ui/merkle/render.js +1 -0
- package/src/ui/runtime-activity.js +192 -0
- package/src/ui/theme.js +19 -18
- package/test/account-session.test.mjs +36 -0
- package/test/app-building-skills.test.mjs +61 -0
- package/test/apps-scaffold-contract.test.mjs +56 -0
- package/test/capabilities.test.mjs +53 -4
- package/test/cli-preferences.test.mjs +46 -0
- package/test/cloudflare-connector.test.mjs +96 -0
- package/test/cloudflare-runtime.test.mjs +75 -0
- package/test/context.test.mjs +144 -0
- package/test/deploy-health-scan.test.mjs +67 -0
- package/test/error-diagnostics.test.mjs +59 -0
- package/test/eval-context.test.mjs +37 -0
- package/test/execution-approvals.test.mjs +27 -0
- package/test/indexing-provider.test.mjs +29 -0
- package/test/jr-dev-skill.test.mjs +26 -0
- package/test/knowledge-context-pack.test.mjs +19 -0
- package/test/knowledge.test.mjs +1 -1
- package/test/local-sessions.test.mjs +42 -0
- package/test/local-studio-deploy.test.mjs +83 -0
- package/test/merkle-persistence.test.mjs +91 -0
- package/test/merkle.test.mjs +77 -3
- package/test/model-catalog.test.mjs +43 -0
- package/test/models.test.mjs +51 -0
- package/test/npm10-lock.test.mjs +29 -0
- package/test/ollama.test.mjs +94 -0
- package/test/openai-responses.test.mjs +95 -0
- package/test/project-config.test.mjs +81 -0
- package/test/project-rules.test.mjs +44 -0
- package/test/provider-credentials.test.mjs +52 -0
- package/test/rehydrate.test.mjs +25 -0
- package/test/release-hygiene.test.mjs +34 -0
- package/test/repository-snapshot-view.test.mjs +112 -0
- package/test/responses-runner.test.mjs +148 -0
- package/test/runtime-activity.test.mjs +98 -0
- package/test/sdk-worker-contract.test.mjs +68 -0
- package/test/security.test.mjs +46 -0
- package/test/shell.test.mjs +55 -21
- package/test/skills.test.mjs +22 -0
- package/test/smoke.mjs +5 -2
- package/test/telemetry.test.mjs +79 -0
- package/test/theme-portability.test.mjs +14 -0
- package/test/tools-search.test.mjs +40 -0
- package/test/whoami-resume.test.mjs +56 -0
- package/examples/agentsam-tui-ansi.mjs +0 -149
- package/src/commands/tui.js +0 -120
- package/src/ui/splash-xterm.js +0 -290
- package/src/ui/splash.js +0 -426
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { getSkill, listSkills, loadSkill } from '../skills/index.js';
|
|
2
|
+
|
|
3
|
+
function parse(argv = []) {
|
|
4
|
+
const out = { id: '', json: false, references: false };
|
|
5
|
+
for (const arg of argv) {
|
|
6
|
+
if (arg === '--json') out.json = true;
|
|
7
|
+
else if (arg === '--references' || arg === '--full') out.references = true;
|
|
8
|
+
else if (arg === '--help' || arg === '-h') out.help = true;
|
|
9
|
+
else if (arg.startsWith('-')) throw new Error(`Unknown skills option: ${arg}`);
|
|
10
|
+
else if (!out.id) out.id = arg;
|
|
11
|
+
else throw new Error('Only one skill id or alias may be supplied.');
|
|
12
|
+
}
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function help() {
|
|
17
|
+
return [
|
|
18
|
+
'Agent Sam · skills',
|
|
19
|
+
' agentsam skills list portable skills',
|
|
20
|
+
' agentsam skills <id-or-alias> print compact skill instructions',
|
|
21
|
+
' agentsam skills <id-or-alias> --references include on-demand references',
|
|
22
|
+
' agentsam skills <id-or-alias> --json machine-readable skill payload',
|
|
23
|
+
'',
|
|
24
|
+
'Examples:',
|
|
25
|
+
' agentsam skills quick-bytes',
|
|
26
|
+
' agentsam skills no-regress --references',
|
|
27
|
+
'',
|
|
28
|
+
].join('\n');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function runSkills(argv = []) {
|
|
32
|
+
const opts = parse(argv);
|
|
33
|
+
if (opts.help) {
|
|
34
|
+
process.stdout.write(help());
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!opts.id) {
|
|
39
|
+
const skills = listSkills();
|
|
40
|
+
if (opts.json) process.stdout.write(`${JSON.stringify(skills, null, 2)}\n`);
|
|
41
|
+
else {
|
|
42
|
+
process.stdout.write('Agent Sam · portable skills\n\n');
|
|
43
|
+
for (const skill of skills) {
|
|
44
|
+
const aliases = (skill.aliases || []).length ? ` (${skill.aliases.join(', ')})` : '';
|
|
45
|
+
process.stdout.write(` ${skill.id}${aliases}\n ${skill.description}\n`);
|
|
46
|
+
}
|
|
47
|
+
process.stdout.write('\n');
|
|
48
|
+
}
|
|
49
|
+
return skills;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!getSkill(opts.id)) throw new Error(`Unknown skill: ${opts.id}`);
|
|
53
|
+
const loaded = loadSkill(opts.id, { references: opts.references });
|
|
54
|
+
if (opts.json) {
|
|
55
|
+
process.stdout.write(`${JSON.stringify(loaded, null, 2)}\n`);
|
|
56
|
+
return loaded;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
process.stdout.write(`${loaded.instructions.trimEnd()}\n`);
|
|
60
|
+
if (loaded.references?.length) {
|
|
61
|
+
for (const reference of loaded.references) {
|
|
62
|
+
process.stdout.write(`\n---\nReference: ${reference.path}\n---\n\n${reference.content.trimEnd()}\n`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return loaded;
|
|
66
|
+
}
|
|
@@ -50,7 +50,7 @@ export async function runStartLocal(opts = {}) {
|
|
|
50
50
|
Project services:
|
|
51
51
|
npm run dev → local Agent Sam API http://127.0.0.1:${config?.dev_port ?? 8787}
|
|
52
52
|
npm run db:status → inspect local SQLite
|
|
53
|
-
|
|
53
|
+
npx agentsam → enter Agent Sam
|
|
54
54
|
|
|
55
55
|
Press Ctrl+C to stop.
|
|
56
56
|
`);
|
package/src/commands/tunnel.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { spawn, spawnSync } from 'node:child_process';
|
|
8
8
|
import { authenticateViaBrowser } from '../lib/auth.js';
|
|
9
9
|
import { postJson } from '../lib/core-client.js';
|
|
10
|
+
import { resolveAccountSdkKey } from '../lib/account-session.js';
|
|
10
11
|
|
|
11
12
|
const DEFAULT_PORT = 3099;
|
|
12
13
|
|
|
@@ -32,7 +33,7 @@ function parseArgs(argv) {
|
|
|
32
33
|
else if (a === '--platform') opts.platform = argv[++i] || opts.platform;
|
|
33
34
|
else if (a === '--shell') opts.shell = argv[++i] || opts.shell;
|
|
34
35
|
else if (a === '--token' && argv[i + 1]) {
|
|
35
|
-
process.env.
|
|
36
|
+
process.env.AGENTSAM_SDK_KEY = argv[++i];
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
return opts;
|
|
@@ -50,12 +51,12 @@ function ensureCloudflared() {
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
async function resolveToken() {
|
|
53
|
-
const existing =
|
|
54
|
+
const existing = resolveAccountSdkKey({ env: process.env }).value;
|
|
54
55
|
if (existing.startsWith('sdk_')) return existing;
|
|
55
56
|
const session = await authenticateViaBrowser();
|
|
56
57
|
const tok = String(session?.access_token || '').trim();
|
|
57
|
-
if (!tok.startsWith('sdk_')) throw new Error('IAM auth did not return an sdk_ bearer
|
|
58
|
-
process.env.
|
|
58
|
+
if (!tok.startsWith('sdk_')) throw new Error('IAM auth did not return an sdk_ bearer key');
|
|
59
|
+
process.env.AGENTSAM_SDK_KEY = tok;
|
|
59
60
|
return tok;
|
|
60
61
|
}
|
|
61
62
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { getJson } from '../lib/core-client.js';
|
|
2
|
+
import { resolveAccountSdkKey } from '../lib/account-session.js';
|
|
3
|
+
import { listProviderCredentialStatus } from '../lib/provider-credentials.js';
|
|
4
|
+
|
|
5
|
+
function writeLine(write, value = '') { write(`${value}\n`); }
|
|
6
|
+
|
|
7
|
+
export async function collectWhoami(options = {}) {
|
|
8
|
+
const env = options.env || process.env;
|
|
9
|
+
const sdk = resolveAccountSdkKey({ env, explicit: options.token || '', home: options.home });
|
|
10
|
+
const token = sdk.value;
|
|
11
|
+
const credentials = listProviderCredentialStatus({ env, home: options.home });
|
|
12
|
+
if (!token) return {
|
|
13
|
+
schema_version: 1,
|
|
14
|
+
authenticated: false,
|
|
15
|
+
authority: 'iam',
|
|
16
|
+
identity: null,
|
|
17
|
+
sdk_credential: { configured: false, source: null },
|
|
18
|
+
provider_credentials: credentials,
|
|
19
|
+
};
|
|
20
|
+
try {
|
|
21
|
+
const loader = options.contextLoader || ((sdkKey) => getJson('/api/sdk/context', sdkKey));
|
|
22
|
+
const context = await loader(token);
|
|
23
|
+
return {
|
|
24
|
+
schema_version: 1,
|
|
25
|
+
authenticated: true,
|
|
26
|
+
authority: 'iam',
|
|
27
|
+
identity: {
|
|
28
|
+
user_id: context?.user_id || null,
|
|
29
|
+
account_id: context?.account_id || null,
|
|
30
|
+
email: context?.email || context?.user?.email || null,
|
|
31
|
+
},
|
|
32
|
+
sdk_credential: { configured: true, source: sdk.source, valid: true },
|
|
33
|
+
provider_credentials: credentials,
|
|
34
|
+
cloudflare_connected: context?.cloudflare?.ok === true,
|
|
35
|
+
byok: context?.byok && typeof context.byok === 'object'
|
|
36
|
+
? Object.fromEntries(Object.entries(context.byok).map(([key, value]) => [key, { configured: value?.configured === true }]))
|
|
37
|
+
: {},
|
|
38
|
+
};
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return {
|
|
41
|
+
schema_version: 1,
|
|
42
|
+
authenticated: false,
|
|
43
|
+
authority: 'iam',
|
|
44
|
+
identity: null,
|
|
45
|
+
sdk_credential: { configured: true, source: sdk.source, valid: false, error: error?.message || String(error) },
|
|
46
|
+
provider_credentials: credentials,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function renderWhoami(status) {
|
|
52
|
+
const lines = ['', ' Agent Sam · whoami', ''];
|
|
53
|
+
if (status.authenticated) {
|
|
54
|
+
lines.push(' authenticated yes');
|
|
55
|
+
lines.push(` authority ${status.authority}`);
|
|
56
|
+
if (status.identity?.email) lines.push(` email ${status.identity.email}`);
|
|
57
|
+
if (status.identity?.account_id) lines.push(` account ${status.identity.account_id}`);
|
|
58
|
+
if (status.identity?.user_id) lines.push(` user ${status.identity.user_id}`);
|
|
59
|
+
} else {
|
|
60
|
+
lines.push(' authenticated no');
|
|
61
|
+
lines.push(` SDK key ${status.sdk_credential?.configured ? 'present but not validated' : 'not configured'}`);
|
|
62
|
+
if (status.sdk_credential?.error) lines.push(` error ${status.sdk_credential.error}`);
|
|
63
|
+
}
|
|
64
|
+
lines.push('');
|
|
65
|
+
lines.push(' Provider credentials');
|
|
66
|
+
for (const row of status.provider_credentials || []) {
|
|
67
|
+
const state = row.configured ? 'available' : row.error ? `blocked (${row.error})` : 'not configured';
|
|
68
|
+
const source = row.source ? ` · ${row.source}` : '';
|
|
69
|
+
lines.push(` ${String(row.provider).padEnd(12)} ${state}${source}`);
|
|
70
|
+
}
|
|
71
|
+
lines.push('');
|
|
72
|
+
lines.push(' Secret values are never printed by whoami.');
|
|
73
|
+
lines.push('');
|
|
74
|
+
return lines.join('\n');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function runWhoami(argv = [], options = {}) {
|
|
78
|
+
const allowed = new Set(['--json']);
|
|
79
|
+
const unknown = argv.filter((arg) => !allowed.has(arg));
|
|
80
|
+
if (unknown.length) throw new Error(`unknown whoami option: ${unknown[0]}`);
|
|
81
|
+
const status = await collectWhoami(options);
|
|
82
|
+
const write = options.write || ((text) => process.stdout.write(text));
|
|
83
|
+
if (argv.includes('--json')) writeLine(write, JSON.stringify(status, null, 2));
|
|
84
|
+
else write(renderWhoami(status));
|
|
85
|
+
return status;
|
|
86
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export const DEFAULT_CONTEXT_RATIOS = Object.freeze({
|
|
2
|
+
targetInputRatio: 0.60,
|
|
3
|
+
hardInputRatio: 0.85,
|
|
4
|
+
charsPerToken: 4,
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
function ratio(value, label) {
|
|
8
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0 || value > 1) throw new RangeError(`${label} must be > 0 and <= 1`);
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function positiveNumber(value, label) {
|
|
13
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) throw new RangeError(`${label} must be positive`);
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function positiveInteger(value, label) {
|
|
18
|
+
if (!Number.isInteger(value) || value < 1) throw new RangeError(`${label} must be a positive integer`);
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function nullablePositiveInteger(value, label) {
|
|
23
|
+
if (value == null) return null;
|
|
24
|
+
return positiveInteger(value, label);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createContextBudget(options = {}) {
|
|
28
|
+
const windowTokens = positiveInteger(options.windowTokens, 'windowTokens');
|
|
29
|
+
const charsPerToken = positiveNumber(options.charsPerToken ?? DEFAULT_CONTEXT_RATIOS.charsPerToken, 'charsPerToken');
|
|
30
|
+
const targetInputRatio = ratio(
|
|
31
|
+
options.targetInputRatio ?? (options.targetInputTokens ? options.targetInputTokens / windowTokens : DEFAULT_CONTEXT_RATIOS.targetInputRatio),
|
|
32
|
+
'targetInputRatio',
|
|
33
|
+
);
|
|
34
|
+
const hardInputRatio = ratio(
|
|
35
|
+
options.hardInputRatio ?? options.hardWindowRatio ?? (options.hardInputTokens ? options.hardInputTokens / windowTokens : DEFAULT_CONTEXT_RATIOS.hardInputRatio),
|
|
36
|
+
'hardInputRatio',
|
|
37
|
+
);
|
|
38
|
+
if (targetInputRatio >= hardInputRatio) throw new RangeError('targetInputRatio must be lower than hardInputRatio');
|
|
39
|
+
|
|
40
|
+
const windowChars = Math.floor(windowTokens * charsPerToken);
|
|
41
|
+
const targetInputTokens = positiveInteger(options.targetInputTokens ?? Math.floor(windowTokens * targetInputRatio), 'targetInputTokens');
|
|
42
|
+
const hardInputTokens = positiveInteger(options.hardInputTokens ?? Math.floor(windowTokens * hardInputRatio), 'hardInputTokens');
|
|
43
|
+
if (targetInputTokens >= hardInputTokens) throw new RangeError('targetInputTokens must be lower than hardInputTokens');
|
|
44
|
+
|
|
45
|
+
const maxNormalInputTokens = positiveInteger(options.maxNormalInputTokens ?? hardInputTokens, 'maxNormalInputTokens');
|
|
46
|
+
if (maxNormalInputTokens > hardInputTokens) throw new RangeError('maxNormalInputTokens must not exceed hardInputTokens');
|
|
47
|
+
const compactAtTokens = positiveInteger(
|
|
48
|
+
options.compactAtTokens ?? Math.floor(targetInputTokens + ((maxNormalInputTokens - targetInputTokens) * 0.4)),
|
|
49
|
+
'compactAtTokens',
|
|
50
|
+
);
|
|
51
|
+
const interveneAtTokens = positiveInteger(
|
|
52
|
+
options.interveneAtTokens ?? Math.floor(targetInputTokens + ((maxNormalInputTokens - targetInputTokens) * 0.72)),
|
|
53
|
+
'interveneAtTokens',
|
|
54
|
+
);
|
|
55
|
+
if (!(targetInputTokens <= compactAtTokens && compactAtTokens <= interveneAtTokens && interveneAtTokens <= maxNormalInputTokens)) {
|
|
56
|
+
throw new RangeError('context policy must satisfy target <= compactAt <= interveneAt <= maxNormal');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const pricingThresholdTokens = nullablePositiveInteger(options.pricingThresholdTokens, 'pricingThresholdTokens');
|
|
60
|
+
const safetyMarginTokens = Number.isInteger(options.safetyMarginTokens) && options.safetyMarginTokens >= 0
|
|
61
|
+
? options.safetyMarginTokens
|
|
62
|
+
: pricingThresholdTokens && pricingThresholdTokens > maxNormalInputTokens
|
|
63
|
+
? pricingThresholdTokens - maxNormalInputTokens
|
|
64
|
+
: 0;
|
|
65
|
+
const targetInputChars = Math.floor(targetInputTokens * charsPerToken);
|
|
66
|
+
const hardInputChars = Math.floor(hardInputTokens * charsPerToken);
|
|
67
|
+
|
|
68
|
+
return Object.freeze({
|
|
69
|
+
windowTokens,
|
|
70
|
+
windowChars,
|
|
71
|
+
charsPerToken,
|
|
72
|
+
targetInputRatio,
|
|
73
|
+
hardInputRatio,
|
|
74
|
+
hardWindowRatio: hardInputRatio,
|
|
75
|
+
targetInputTokens,
|
|
76
|
+
compactAtTokens,
|
|
77
|
+
interveneAtTokens,
|
|
78
|
+
maxNormalInputTokens,
|
|
79
|
+
hardInputTokens,
|
|
80
|
+
pricingThresholdTokens,
|
|
81
|
+
safetyMarginTokens,
|
|
82
|
+
targetInputChars,
|
|
83
|
+
hardInputChars,
|
|
84
|
+
maxSystemChars: positiveInteger(options.maxSystemChars ?? Math.max(4_000, Math.min(60_000, Math.floor(windowChars * 0.05))), 'maxSystemChars'),
|
|
85
|
+
maxToolSchemaChars: positiveInteger(options.maxToolSchemaChars ?? Math.max(4_000, Math.min(40_000, Math.floor(windowChars * 0.04))), 'maxToolSchemaChars'),
|
|
86
|
+
maxEvidenceChars: positiveInteger(options.maxEvidenceChars ?? Math.max(8_000, Math.min(100_000, Math.floor(targetInputChars * 0.25))), 'maxEvidenceChars'),
|
|
87
|
+
maxFileCharsPerRead: positiveInteger(options.maxFileCharsPerRead ?? 32_768, 'maxFileCharsPerRead'),
|
|
88
|
+
maxFileCharsPerTurn: positiveInteger(options.maxFileCharsPerTurn ?? Math.max(32_768, Math.min(131_072, Math.floor(targetInputChars * 0.25))), 'maxFileCharsPerTurn'),
|
|
89
|
+
maxToolResultChars: positiveInteger(options.maxToolResultChars ?? 24_000, 'maxToolResultChars'),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function estimateContextTokens(chars, charsPerToken = DEFAULT_CONTEXT_RATIOS.charsPerToken) {
|
|
94
|
+
if (!Number.isFinite(chars) || chars < 0) throw new RangeError('chars must be a non-negative number');
|
|
95
|
+
if (!Number.isFinite(charsPerToken) || charsPerToken <= 0) throw new RangeError('charsPerToken must be positive');
|
|
96
|
+
return Math.ceil(chars / charsPerToken);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function assessContextUsage(tokens, budget) {
|
|
100
|
+
const inputTokens = Number(tokens);
|
|
101
|
+
if (!Number.isFinite(inputTokens) || inputTokens < 0) throw new RangeError('tokens must be a non-negative number');
|
|
102
|
+
if (!budget?.windowTokens) throw new TypeError('context budget is required');
|
|
103
|
+
let stage = 'normal';
|
|
104
|
+
if (inputTokens >= budget.hardInputTokens) stage = 'hard';
|
|
105
|
+
else if (inputTokens >= budget.maxNormalInputTokens) stage = 'max-normal';
|
|
106
|
+
else if (inputTokens >= budget.interveneAtTokens) stage = 'intervene';
|
|
107
|
+
else if (inputTokens >= budget.compactAtTokens) stage = 'compact';
|
|
108
|
+
else if (inputTokens >= budget.targetInputTokens) stage = 'target';
|
|
109
|
+
const pricingThresholdTokens = budget.pricingThresholdTokens ?? null;
|
|
110
|
+
return Object.freeze({
|
|
111
|
+
inputTokens: Math.ceil(inputTokens),
|
|
112
|
+
stage,
|
|
113
|
+
utilizationRatio: inputTokens / budget.windowTokens,
|
|
114
|
+
pricingThresholdTokens,
|
|
115
|
+
tokensUntilPricingThreshold: pricingThresholdTokens == null ? null : pricingThresholdTokens - Math.ceil(inputTokens),
|
|
116
|
+
pricingThresholdCrossed: pricingThresholdTokens == null ? false : inputTokens > pricingThresholdTokens,
|
|
117
|
+
shouldCompact: inputTokens >= budget.compactAtTokens,
|
|
118
|
+
shouldIntervene: inputTokens >= budget.interveneAtTokens,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { normalizeContextItem } from './resolve.js';
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_CONSUMED_CONTEXT_CHARS = 4_000;
|
|
4
|
+
|
|
5
|
+
export function compactContextItem(value, options = {}) {
|
|
6
|
+
const item = normalizeContextItem(value);
|
|
7
|
+
const maxChars = Number(options.maxChars ?? DEFAULT_CONSUMED_CONTEXT_CHARS);
|
|
8
|
+
if (!Number.isInteger(maxChars) || maxChars < 1) throw new RangeError('compact maxChars must be a positive integer');
|
|
9
|
+
if (item.content.length <= maxChars) return Object.freeze({ ...item, compacted: false });
|
|
10
|
+
const content = `${item.content.slice(0, Math.max(0, maxChars - 1))}…`;
|
|
11
|
+
return Object.freeze({
|
|
12
|
+
...item,
|
|
13
|
+
content,
|
|
14
|
+
chars: content.length,
|
|
15
|
+
compacted: true,
|
|
16
|
+
source_chars: item.content.length,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function compactConsumedToolResult(value, options = {}) {
|
|
21
|
+
const ref = String(options.ref || value?.ref || 'tool_result:consumed').trim();
|
|
22
|
+
const hash = options.hash || value?.hash;
|
|
23
|
+
const priority = Number.isFinite(options.priority) ? options.priority : Number(value?.priority || 0);
|
|
24
|
+
const content = typeof value === 'string'
|
|
25
|
+
? value
|
|
26
|
+
: String(value?.content ?? JSON.stringify(value?.content ?? value ?? null));
|
|
27
|
+
return compactContextItem({ ref, kind: 'tool_result', hash, priority, content }, options);
|
|
28
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { DEFAULT_CONTEXT_RATIOS, createContextBudget, estimateContextTokens, assessContextUsage } from './budget.js';
|
|
2
|
+
export { DEFAULT_RESULT_POLICY, RESULT_DETAIL_ORDER, normalizeResultPolicy, truncateResultText, boundResultItems } from './result-policy.js';
|
|
3
|
+
export { CONTEXT_ITEM_KINDS, normalizeContextItem, resolveContext, resolveProjectContext } from './resolve.js';
|
|
4
|
+
export { PROJECT_RULES_FILENAME, DEFAULT_PROJECT_RULES_MAX_CHARS, defaultProjectRules, ensureProjectRules, findProjectRules, loadProjectRules } from '../lib/project-rules.js';
|
|
5
|
+
export { AGENT_RUNTIME_FILENAME, AGENT_INSTRUCTION_PRECEDENCE, findAgentRuntimeContract, compileAgentInstructions } from '../lib/agent-instructions.js';
|
|
6
|
+
export { DEFAULT_CONSUMED_CONTEXT_CHARS, compactContextItem, compactConsumedToolResult } from './compact.js';
|
|
7
|
+
export { rehydrateContextRef } from './rehydrate.js';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { normalizeContextItem } from './resolve.js';
|
|
3
|
+
|
|
4
|
+
function sha256(value) { return `sha256:${createHash('sha256').update(String(value)).digest('hex')}`; }
|
|
5
|
+
|
|
6
|
+
export async function rehydrateContextRef(ref, adapter, options = {}) {
|
|
7
|
+
const sourceRef = String(ref || '').trim();
|
|
8
|
+
if (!sourceRef) throw new TypeError('rehydrate ref is required');
|
|
9
|
+
const read = typeof adapter === 'function' ? adapter : adapter?.read?.bind(adapter);
|
|
10
|
+
if (typeof read !== 'function') throw new TypeError('rehydration adapter must provide read(ref, options)');
|
|
11
|
+
const maxChars = Number(options.maxChars ?? 64_000);
|
|
12
|
+
if (!Number.isInteger(maxChars) || maxChars < 1) throw new RangeError('rehydration maxChars must be a positive integer');
|
|
13
|
+
|
|
14
|
+
const raw = await read(sourceRef, { maxChars, signal: options.signal });
|
|
15
|
+
if (raw == null) throw new Error(`rehydration_source_not_found:${sourceRef}`);
|
|
16
|
+
const value = typeof raw === 'string' ? { content: raw } : raw;
|
|
17
|
+
const content = String(value.content ?? '');
|
|
18
|
+
const computedHash = sha256(content);
|
|
19
|
+
const expectedHash = String(options.expectedHash || value.hash || '').trim();
|
|
20
|
+
if (expectedHash && expectedHash.startsWith('sha256:') && expectedHash !== computedHash) {
|
|
21
|
+
throw new Error(`rehydration_hash_mismatch:${sourceRef}`);
|
|
22
|
+
}
|
|
23
|
+
const bounded = content.length > maxChars ? `${content.slice(0, Math.max(0, maxChars - 1))}…` : content;
|
|
24
|
+
const item = normalizeContextItem({
|
|
25
|
+
ref: sourceRef,
|
|
26
|
+
kind: options.kind || value.kind || 'artifact',
|
|
27
|
+
hash: expectedHash || computedHash,
|
|
28
|
+
priority: Number.isFinite(options.priority) ? options.priority : Number(value.priority || 0),
|
|
29
|
+
content: bounded,
|
|
30
|
+
chars: bounded.length,
|
|
31
|
+
truncated: bounded.length < content.length,
|
|
32
|
+
source_chars: content.length > bounded.length ? content.length : undefined,
|
|
33
|
+
});
|
|
34
|
+
return Object.freeze({ ...item, truncated: bounded.length < content.length });
|
|
35
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { assessContextUsage, createContextBudget, estimateContextTokens } from './budget.js';
|
|
2
|
+
import { compileAgentInstructions } from '../lib/agent-instructions.js';
|
|
3
|
+
|
|
4
|
+
export const CONTEXT_ITEM_KINDS = Object.freeze(['file', 'symbol', 'memory', 'tool_result', 'repo', 'artifact']);
|
|
5
|
+
|
|
6
|
+
function clean(value) { return value == null ? '' : String(value).trim(); }
|
|
7
|
+
function nonNegativeInteger(value) { return Number.isInteger(value) && value >= 0 ? value : 0; }
|
|
8
|
+
|
|
9
|
+
export function normalizeContextItem(value = {}) {
|
|
10
|
+
const ref = clean(value.ref);
|
|
11
|
+
if (!ref) throw new TypeError('ContextItem.ref is required');
|
|
12
|
+
const kind = clean(value.kind);
|
|
13
|
+
if (!CONTEXT_ITEM_KINDS.includes(kind)) throw new RangeError(`ContextItem.kind must be one of: ${CONTEXT_ITEM_KINDS.join(', ')}`);
|
|
14
|
+
const content = value.content == null ? '' : String(value.content);
|
|
15
|
+
const chars = Number.isInteger(value.chars) && value.chars >= 0 ? value.chars : content.length;
|
|
16
|
+
const priority = Number.isFinite(value.priority) ? Number(value.priority) : 0;
|
|
17
|
+
return Object.freeze({
|
|
18
|
+
ref,
|
|
19
|
+
kind,
|
|
20
|
+
chars,
|
|
21
|
+
hash: clean(value.hash) || undefined,
|
|
22
|
+
priority,
|
|
23
|
+
content,
|
|
24
|
+
...(value.truncated === true ? { truncated: true } : {}),
|
|
25
|
+
...(value.compacted === true ? { compacted: true } : {}),
|
|
26
|
+
...(Number.isInteger(value.source_chars) && value.source_chars >= chars ? { source_chars: value.source_chars } : {}),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function capForKind(item, budget) {
|
|
31
|
+
if (item.kind === 'file') return budget.maxFileCharsPerRead;
|
|
32
|
+
if (item.kind === 'tool_result') return budget.maxToolResultChars;
|
|
33
|
+
return budget.maxEvidenceChars;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function truncate(content, maxChars) {
|
|
37
|
+
if (content.length <= maxChars) return { content, truncated: false };
|
|
38
|
+
return { content: `${content.slice(0, Math.max(0, maxChars - 1))}…`, truncated: true };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function resolveContext(options = {}) {
|
|
42
|
+
const objective = clean(options.objective);
|
|
43
|
+
if (!objective) throw new TypeError('context.resolve objective is required');
|
|
44
|
+
const budget = options.budget?.windowTokens ? options.budget : createContextBudget(options.budget || {});
|
|
45
|
+
const refs = [...new Set((options.refs || []).map(clean).filter(Boolean))];
|
|
46
|
+
const candidates = (options.items || []).map((item, index) => ({ item: normalizeContextItem(item), index }));
|
|
47
|
+
candidates.sort((a, b) => (b.item.priority - a.item.priority) || (a.index - b.index));
|
|
48
|
+
|
|
49
|
+
let selectedChars = 0;
|
|
50
|
+
let fileChars = 0;
|
|
51
|
+
const included = [];
|
|
52
|
+
const deferred = [];
|
|
53
|
+
|
|
54
|
+
for (const row of candidates) {
|
|
55
|
+
const item = row.item;
|
|
56
|
+
const remainingEvidence = budget.maxEvidenceChars - selectedChars;
|
|
57
|
+
if (remainingEvidence <= 0) { deferred.push(item.ref); continue; }
|
|
58
|
+
const kindCap = capForKind(item, budget);
|
|
59
|
+
const remainingFile = item.kind === 'file' ? budget.maxFileCharsPerTurn - fileChars : Number.POSITIVE_INFINITY;
|
|
60
|
+
const allowed = Math.floor(Math.min(kindCap, remainingEvidence, remainingFile));
|
|
61
|
+
if (allowed <= 0) { deferred.push(item.ref); continue; }
|
|
62
|
+
const clipped = truncate(item.content, allowed);
|
|
63
|
+
const next = Object.freeze({ ...item, content: clipped.content, chars: clipped.content.length, truncated: clipped.truncated || item.truncated === true || item.chars > clipped.content.length });
|
|
64
|
+
included.push(next);
|
|
65
|
+
selectedChars += next.chars;
|
|
66
|
+
if (item.kind === 'file') fileChars += next.chars;
|
|
67
|
+
if (clipped.truncated) deferred.push(`${item.ref}#remainder`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const ruleChars = options.rules?.content ? String(options.rules.content).length : 0;
|
|
71
|
+
const instructionChars = ruleChars + nonNegativeInteger(options.instructionChars);
|
|
72
|
+
const toolSchemaChars = nonNegativeInteger(options.toolSchemaChars);
|
|
73
|
+
const historyChars = nonNegativeInteger(options.historyChars);
|
|
74
|
+
const toolResultChars = included.filter((item) => item.kind === 'tool_result').reduce((sum, item) => sum + item.chars, 0);
|
|
75
|
+
const evidenceChars = selectedChars - toolResultChars;
|
|
76
|
+
const totalChars = instructionChars + toolSchemaChars + historyChars + evidenceChars + toolResultChars;
|
|
77
|
+
const estimatedInputTokens = estimateContextTokens(totalChars, budget.charsPerToken);
|
|
78
|
+
const pressure = assessContextUsage(estimatedInputTokens, budget);
|
|
79
|
+
const rehydratable = [...new Set(included.filter((item) => item.ref).map((item) => item.ref))];
|
|
80
|
+
|
|
81
|
+
return Object.freeze({
|
|
82
|
+
objective,
|
|
83
|
+
refs: Object.freeze(refs),
|
|
84
|
+
rules: options.rules || null,
|
|
85
|
+
items: Object.freeze(included),
|
|
86
|
+
budget,
|
|
87
|
+
receipt: Object.freeze({
|
|
88
|
+
chars: totalChars,
|
|
89
|
+
system_chars: instructionChars,
|
|
90
|
+
instruction_chars: instructionChars,
|
|
91
|
+
tool_schema_chars: toolSchemaChars,
|
|
92
|
+
history_chars: historyChars,
|
|
93
|
+
evidence_chars: evidenceChars,
|
|
94
|
+
tool_result_chars: toolResultChars,
|
|
95
|
+
estimated_tokens: estimatedInputTokens,
|
|
96
|
+
estimated_input_tokens: estimatedInputTokens,
|
|
97
|
+
window_tokens: budget.windowTokens,
|
|
98
|
+
utilization_ratio: pressure.utilizationRatio,
|
|
99
|
+
pricing_threshold_tokens: pressure.pricingThresholdTokens,
|
|
100
|
+
tokens_until_pricing_threshold: pressure.tokensUntilPricingThreshold,
|
|
101
|
+
pressure: pressure.stage,
|
|
102
|
+
estimate_kind: 'local',
|
|
103
|
+
sources_considered: candidates.length,
|
|
104
|
+
sources_included: included.length,
|
|
105
|
+
sources_deferred: deferred.length,
|
|
106
|
+
deferred_refs: Object.freeze(deferred),
|
|
107
|
+
rehydratable_refs: Object.freeze(rehydratable),
|
|
108
|
+
}),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function resolveProjectContext(options = {}) {
|
|
113
|
+
const budget = options.budget?.windowTokens ? options.budget : createContextBudget(options.budget || {});
|
|
114
|
+
const rules = compileAgentInstructions(options.cwd || process.cwd(), { maxChars: Math.min(24_000, budget.maxSystemChars) });
|
|
115
|
+
return resolveContext({ ...options, budget, rules });
|
|
116
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export const RESULT_DETAIL_ORDER = Object.freeze(['metadata', 'card', 'excerpt', 'range', 'full']);
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_RESULT_POLICY = Object.freeze({
|
|
4
|
+
max_items: 8,
|
|
5
|
+
max_chars: 24_000,
|
|
6
|
+
detail: 'excerpt',
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
function positiveInteger(value, label) {
|
|
10
|
+
if (!Number.isInteger(value) || value < 1) throw new RangeError(`${label} must be a positive integer`);
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function detailRank(value) {
|
|
15
|
+
const index = RESULT_DETAIL_ORDER.indexOf(value);
|
|
16
|
+
if (index === -1) throw new RangeError(`detail must be one of: ${RESULT_DETAIL_ORDER.join(', ')}`);
|
|
17
|
+
return index;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function normalizeResultPolicy(value = {}, options = {}) {
|
|
21
|
+
const ceiling = { ...DEFAULT_RESULT_POLICY, ...(options.ceiling || {}) };
|
|
22
|
+
const requested = {
|
|
23
|
+
max_items: positiveInteger(value.max_items ?? ceiling.max_items, 'max_items'),
|
|
24
|
+
max_chars: positiveInteger(value.max_chars ?? ceiling.max_chars, 'max_chars'),
|
|
25
|
+
detail: value.detail ?? ceiling.detail,
|
|
26
|
+
};
|
|
27
|
+
detailRank(requested.detail);
|
|
28
|
+
detailRank(ceiling.detail);
|
|
29
|
+
|
|
30
|
+
const higherDetailOperation = options.operation === 'higher-detail';
|
|
31
|
+
if (!higherDetailOperation) {
|
|
32
|
+
if (requested.max_items > ceiling.max_items) throw new RangeError('result_policy_higher_detail_required:max_items');
|
|
33
|
+
if (requested.max_chars > ceiling.max_chars) throw new RangeError('result_policy_higher_detail_required:max_chars');
|
|
34
|
+
if (detailRank(requested.detail) > detailRank(ceiling.detail)) throw new RangeError('result_policy_higher_detail_required:detail');
|
|
35
|
+
}
|
|
36
|
+
return Object.freeze(requested);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function truncateResultText(value, maxChars = DEFAULT_RESULT_POLICY.max_chars) {
|
|
40
|
+
const text = String(value ?? '');
|
|
41
|
+
positiveInteger(maxChars, 'maxChars');
|
|
42
|
+
if (text.length <= maxChars) return Object.freeze({ text, chars: text.length, source_chars: text.length, truncated: false });
|
|
43
|
+
const bounded = `${text.slice(0, Math.max(0, maxChars - 1))}…`;
|
|
44
|
+
return Object.freeze({ text: bounded, chars: bounded.length, source_chars: text.length, truncated: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function boundResultItems(items = [], requested = {}, options = {}) {
|
|
48
|
+
if (!Array.isArray(items)) throw new TypeError('items must be an array');
|
|
49
|
+
const policy = normalizeResultPolicy(requested, options);
|
|
50
|
+
const selected = items.slice(0, policy.max_items);
|
|
51
|
+
const serialized = JSON.stringify(selected);
|
|
52
|
+
const bounded = truncateResultText(serialized, policy.max_chars);
|
|
53
|
+
return Object.freeze({
|
|
54
|
+
items: Object.freeze(selected),
|
|
55
|
+
policy,
|
|
56
|
+
receipt: Object.freeze({
|
|
57
|
+
source_items: items.length,
|
|
58
|
+
returned_items: selected.length,
|
|
59
|
+
omitted_items: Math.max(0, items.length - selected.length),
|
|
60
|
+
serialized_chars: serialized.length,
|
|
61
|
+
max_chars: policy.max_chars,
|
|
62
|
+
detail: policy.detail,
|
|
63
|
+
serialized_overflow: bounded.truncated,
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
}
|