@inneranimalmedia/agentsam-sdk 2.5.0 → 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.
Files changed (112) hide show
  1. package/AGENTSAM.md +55 -0
  2. package/README.md +12 -8
  3. package/bin/agentsam +2 -0
  4. package/docs/AGENTSAM_ASTRA_OPENAI_INTEGRATION.md +1363 -0
  5. package/docs/CLI_SHELL.md +163 -53
  6. package/docs/RELEASES.md +16 -7
  7. package/package.json +20 -8
  8. package/packages/connectors/cloudflare/package.json +10 -0
  9. package/packages/connectors/cloudflare/src/index.js +127 -0
  10. package/packages/connectors/cloudflare/src/owner.js +76 -0
  11. package/packages/connectors/cloudflare/src/routes.js +223 -0
  12. package/packages/connectors/cloudflare/src/vault.js +80 -0
  13. package/packages/connectors/cloudflare/tests/connector.test.mjs +44 -0
  14. package/packages/identity/package.json +2 -2
  15. package/packages/identity/src/contracts/auth-config.js +18 -7
  16. package/packages/identity/tests/auth-config.test.mjs +9 -5
  17. package/packages/identity/tests/oauth-credentials.test.mjs +4 -4
  18. package/protocol/README.md +1 -0
  19. package/protocol/capabilities/cloudflare-cpu-audit-input.schema.json +19 -0
  20. package/protocol/capabilities/cloudflare-cpu-profile-input.schema.json +13 -0
  21. package/protocol/capabilities/cloudflare-wrangler-native-input.schema.json +19 -0
  22. package/protocol/capabilities/manifest.json +47 -0
  23. package/protocol/context/context-budget.schema.json +10 -15
  24. package/protocol/context/context-item.schema.json +4 -5
  25. package/protocol/context/resolved-context-pack.schema.json +19 -14
  26. package/protocol/models/README.md +373 -0
  27. package/protocol/models/model-inventory-v2.schema.json +212 -0
  28. package/skills/agentsam-cloudflare-workers/SKILL.md +53 -0
  29. package/skills/agentsam-cloudflare-workers/references/cpu-profiling.md +16 -0
  30. package/skills/agentsam-cloudflare-workers/references/errors-and-observability.md +29 -0
  31. package/skills/agentsam-cloudflare-workers/references/wrangler-native-map.md +28 -0
  32. package/skills/catalog.json +18 -0
  33. package/src/agent/capability-adapter.js +25 -13
  34. package/src/agent/index.js +1 -0
  35. package/src/agent/responses-runner.js +325 -0
  36. package/src/cli.js +98 -28
  37. package/src/cloudflare/cpu-profile.js +115 -0
  38. package/src/cloudflare/index.js +14 -0
  39. package/src/cloudflare/wrangler.js +132 -0
  40. package/src/commands/account-auth.js +47 -0
  41. package/src/commands/cloudflare.js +58 -0
  42. package/src/commands/connections.js +93 -0
  43. package/src/commands/context-economics.js +114 -0
  44. package/src/commands/deploy.js +39 -3
  45. package/src/commands/eval.js +63 -0
  46. package/src/commands/interactive.js +2 -5
  47. package/src/commands/models.js +85 -40
  48. package/src/commands/preferences.js +101 -59
  49. package/src/commands/resume.js +67 -0
  50. package/src/commands/security.js +5 -3
  51. package/src/commands/shell.js +370 -109
  52. package/src/commands/tunnel.js +2 -2
  53. package/src/commands/whoami.js +86 -0
  54. package/src/context/budget.js +68 -6
  55. package/src/context/index.js +3 -1
  56. package/src/context/rehydrate.js +35 -0
  57. package/src/context/resolve.js +44 -12
  58. package/src/errors/diagnostic.js +160 -0
  59. package/src/errors/index.js +9 -0
  60. package/src/eval/context.js +191 -0
  61. package/src/eval/index.js +1 -0
  62. package/src/index.js +55 -1
  63. package/src/lib/account-session.js +98 -0
  64. package/src/lib/agent-instructions.js +73 -0
  65. package/src/lib/auth.js +4 -0
  66. package/src/lib/cli-preferences.js +28 -24
  67. package/src/lib/deploy/git-guard.js +69 -0
  68. package/src/lib/deploy/health.js +57 -0
  69. package/src/lib/deploy/local-studio.js +283 -0
  70. package/src/lib/deploy/secret-scan.js +65 -0
  71. package/src/lib/detect-context.js +2 -2
  72. package/src/lib/execution-approvals.js +59 -0
  73. package/src/lib/local-sessions.js +127 -0
  74. package/src/lib/provider-credentials.js +83 -0
  75. package/src/lib/scaffold/templates/worker-api/index.js +101 -20
  76. package/src/lib/scaffold/wizards/worker-api.js +27 -11
  77. package/src/lib/slash-commands.js +22 -16
  78. package/src/models/catalog.js +135 -0
  79. package/src/models/index.js +7 -0
  80. package/src/providers/index.js +5 -0
  81. package/src/providers/openai-responses.js +275 -0
  82. package/src/security/process.js +35 -9
  83. package/src/telemetry/contracts.js +203 -0
  84. package/src/telemetry/events.js +48 -0
  85. package/src/telemetry/index.js +8 -0
  86. package/src/tools/hydrate.js +35 -0
  87. package/src/tools/index.js +1 -0
  88. package/src/ui/boot.js +15 -17
  89. package/test/account-session.test.mjs +36 -0
  90. package/test/cli-preferences.test.mjs +26 -5
  91. package/test/cloudflare-connector.test.mjs +96 -0
  92. package/test/cloudflare-runtime.test.mjs +75 -0
  93. package/test/context.test.mjs +61 -12
  94. package/test/deploy-health-scan.test.mjs +67 -0
  95. package/test/error-diagnostics.test.mjs +59 -0
  96. package/test/eval-context.test.mjs +37 -0
  97. package/test/execution-approvals.test.mjs +27 -0
  98. package/test/local-sessions.test.mjs +42 -0
  99. package/test/local-studio-deploy.test.mjs +83 -0
  100. package/test/model-catalog.test.mjs +43 -0
  101. package/test/models.test.mjs +30 -16
  102. package/test/npm10-lock.test.mjs +29 -0
  103. package/test/openai-responses.test.mjs +95 -0
  104. package/test/provider-credentials.test.mjs +52 -0
  105. package/test/rehydrate.test.mjs +25 -0
  106. package/test/release-hygiene.test.mjs +4 -4
  107. package/test/responses-runner.test.mjs +148 -0
  108. package/test/shell.test.mjs +47 -20
  109. package/test/smoke.mjs +4 -1
  110. package/test/telemetry.test.mjs +79 -0
  111. package/test/tools-search.test.mjs +14 -1
  112. package/test/whoami-resume.test.mjs +56 -0
@@ -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
+ }
@@ -19,18 +19,51 @@ function positiveInteger(value, label) {
19
19
  return value;
20
20
  }
21
21
 
22
+ function nullablePositiveInteger(value, label) {
23
+ if (value == null) return null;
24
+ return positiveInteger(value, label);
25
+ }
26
+
22
27
  export function createContextBudget(options = {}) {
23
28
  const windowTokens = positiveInteger(options.windowTokens, 'windowTokens');
24
29
  const charsPerToken = positiveNumber(options.charsPerToken ?? DEFAULT_CONTEXT_RATIOS.charsPerToken, 'charsPerToken');
25
- const targetInputRatio = ratio(options.targetInputRatio ?? DEFAULT_CONTEXT_RATIOS.targetInputRatio, 'targetInputRatio');
26
- const hardInputRatio = ratio(options.hardInputRatio ?? DEFAULT_CONTEXT_RATIOS.hardInputRatio, 'hardInputRatio');
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
+ );
27
38
  if (targetInputRatio >= hardInputRatio) throw new RangeError('targetInputRatio must be lower than hardInputRatio');
28
39
 
29
40
  const windowChars = Math.floor(windowTokens * charsPerToken);
30
- const targetInputTokens = Math.floor(windowTokens * targetInputRatio);
31
- const hardInputTokens = Math.floor(windowTokens * hardInputRatio);
32
- const targetInputChars = Math.floor(windowChars * targetInputRatio);
33
- const hardInputChars = Math.floor(windowChars * hardInputRatio);
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);
34
67
 
35
68
  return Object.freeze({
36
69
  windowTokens,
@@ -38,8 +71,14 @@ export function createContextBudget(options = {}) {
38
71
  charsPerToken,
39
72
  targetInputRatio,
40
73
  hardInputRatio,
74
+ hardWindowRatio: hardInputRatio,
41
75
  targetInputTokens,
76
+ compactAtTokens,
77
+ interveneAtTokens,
78
+ maxNormalInputTokens,
42
79
  hardInputTokens,
80
+ pricingThresholdTokens,
81
+ safetyMarginTokens,
43
82
  targetInputChars,
44
83
  hardInputChars,
45
84
  maxSystemChars: positiveInteger(options.maxSystemChars ?? Math.max(4_000, Math.min(60_000, Math.floor(windowChars * 0.05))), 'maxSystemChars'),
@@ -56,3 +95,26 @@ export function estimateContextTokens(chars, charsPerToken = DEFAULT_CONTEXT_RAT
56
95
  if (!Number.isFinite(charsPerToken) || charsPerToken <= 0) throw new RangeError('charsPerToken must be positive');
57
96
  return Math.ceil(chars / charsPerToken);
58
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
+ }
@@ -1,5 +1,7 @@
1
- export { DEFAULT_CONTEXT_RATIOS, createContextBudget, estimateContextTokens } from './budget.js';
1
+ export { DEFAULT_CONTEXT_RATIOS, createContextBudget, estimateContextTokens, assessContextUsage } from './budget.js';
2
2
  export { DEFAULT_RESULT_POLICY, RESULT_DETAIL_ORDER, normalizeResultPolicy, truncateResultText, boundResultItems } from './result-policy.js';
3
3
  export { CONTEXT_ITEM_KINDS, normalizeContextItem, resolveContext, resolveProjectContext } from './resolve.js';
4
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';
5
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
+ }
@@ -1,9 +1,10 @@
1
- import { createContextBudget, estimateContextTokens } from './budget.js';
2
- import { loadProjectRules } from '../lib/project-rules.js';
1
+ import { assessContextUsage, createContextBudget, estimateContextTokens } from './budget.js';
2
+ import { compileAgentInstructions } from '../lib/agent-instructions.js';
3
3
 
4
4
  export const CONTEXT_ITEM_KINDS = Object.freeze(['file', 'symbol', 'memory', 'tool_result', 'repo', 'artifact']);
5
5
 
6
6
  function clean(value) { return value == null ? '' : String(value).trim(); }
7
+ function nonNegativeInteger(value) { return Number.isInteger(value) && value >= 0 ? value : 0; }
7
8
 
8
9
  export function normalizeContextItem(value = {}) {
9
10
  const ref = clean(value.ref);
@@ -13,7 +14,17 @@ export function normalizeContextItem(value = {}) {
13
14
  const content = value.content == null ? '' : String(value.content);
14
15
  const chars = Number.isInteger(value.chars) && value.chars >= 0 ? value.chars : content.length;
15
16
  const priority = Number.isFinite(value.priority) ? Number(value.priority) : 0;
16
- return Object.freeze({ ref, kind, chars, hash: clean(value.hash) || undefined, priority, content });
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
+ });
17
28
  }
18
29
 
19
30
  function capForKind(item, budget) {
@@ -35,29 +46,38 @@ export function resolveContext(options = {}) {
35
46
  const candidates = (options.items || []).map((item, index) => ({ item: normalizeContextItem(item), index }));
36
47
  candidates.sort((a, b) => (b.item.priority - a.item.priority) || (a.index - b.index));
37
48
 
38
- let evidenceChars = 0;
49
+ let selectedChars = 0;
39
50
  let fileChars = 0;
40
51
  const included = [];
41
52
  const deferred = [];
42
53
 
43
54
  for (const row of candidates) {
44
55
  const item = row.item;
45
- const remainingEvidence = budget.maxEvidenceChars - evidenceChars;
56
+ const remainingEvidence = budget.maxEvidenceChars - selectedChars;
46
57
  if (remainingEvidence <= 0) { deferred.push(item.ref); continue; }
47
58
  const kindCap = capForKind(item, budget);
48
59
  const remainingFile = item.kind === 'file' ? budget.maxFileCharsPerTurn - fileChars : Number.POSITIVE_INFINITY;
49
60
  const allowed = Math.floor(Math.min(kindCap, remainingEvidence, remainingFile));
50
61
  if (allowed <= 0) { deferred.push(item.ref); continue; }
51
62
  const clipped = truncate(item.content, allowed);
52
- const next = Object.freeze({ ...item, content: clipped.content, chars: clipped.content.length, truncated: clipped.truncated || item.chars > clipped.content.length });
63
+ const next = Object.freeze({ ...item, content: clipped.content, chars: clipped.content.length, truncated: clipped.truncated || item.truncated === true || item.chars > clipped.content.length });
53
64
  included.push(next);
54
- evidenceChars += next.chars;
65
+ selectedChars += next.chars;
55
66
  if (item.kind === 'file') fileChars += next.chars;
56
67
  if (clipped.truncated) deferred.push(`${item.ref}#remainder`);
57
68
  }
58
69
 
59
- const systemChars = options.rules?.content ? String(options.rules.content).length : 0;
60
- const totalChars = systemChars + evidenceChars;
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
+
61
81
  return Object.freeze({
62
82
  objective,
63
83
  refs: Object.freeze(refs),
@@ -66,19 +86,31 @@ export function resolveContext(options = {}) {
66
86
  budget,
67
87
  receipt: Object.freeze({
68
88
  chars: totalChars,
89
+ system_chars: instructionChars,
90
+ instruction_chars: instructionChars,
91
+ tool_schema_chars: toolSchemaChars,
92
+ history_chars: historyChars,
69
93
  evidence_chars: evidenceChars,
70
- system_chars: systemChars,
71
- estimated_tokens: estimateContextTokens(totalChars, budget.charsPerToken),
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',
72
103
  sources_considered: candidates.length,
73
104
  sources_included: included.length,
74
105
  sources_deferred: deferred.length,
75
106
  deferred_refs: Object.freeze(deferred),
107
+ rehydratable_refs: Object.freeze(rehydratable),
76
108
  }),
77
109
  });
78
110
  }
79
111
 
80
112
  export function resolveProjectContext(options = {}) {
81
113
  const budget = options.budget?.windowTokens ? options.budget : createContextBudget(options.budget || {});
82
- const rules = loadProjectRules(options.cwd || process.cwd(), { maxChars: Math.min(24_000, budget.maxSystemChars) });
114
+ const rules = compileAgentInstructions(options.cwd || process.cwd(), { maxChars: Math.min(24_000, budget.maxSystemChars) });
83
115
  return resolveContext({ ...options, budget, rules });
84
116
  }
@@ -0,0 +1,160 @@
1
+ const DEFAULT_MAX_DETAIL_CHARS = 12_000;
2
+ const SECRET_KEY = /(?:authorization|api[-_]?key|access[-_]?token|refresh[-_]?token|password|secret|cookie|credential)/i;
3
+ const SECRET_VALUE_PATTERNS = [
4
+ /\bBearer\s+[A-Za-z0-9._~+\/-]+=*/gi,
5
+ /\bsk-[A-Za-z0-9_-]{12,}\b/g,
6
+ /\bsdk_[A-Za-z0-9_-]{8,}\b/g,
7
+ /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g,
8
+ ];
9
+
10
+ function clean(value) { return value == null ? '' : String(value).trim(); }
11
+ function bounded(value, maxChars = DEFAULT_MAX_DETAIL_CHARS) {
12
+ const text = String(value ?? '');
13
+ if (text.length <= maxChars) return text;
14
+ return `${text.slice(0, Math.max(0, maxChars - 1))}…`;
15
+ }
16
+
17
+ function redactString(value, maxChars) {
18
+ let text = bounded(value, maxChars);
19
+ for (const pattern of SECRET_VALUE_PATTERNS) text = text.replace(pattern, '[REDACTED]');
20
+ return text;
21
+ }
22
+
23
+ export function redactDiagnosticValue(value, options = {}, depth = 0, key = '') {
24
+ const maxChars = Number.isInteger(options.maxChars) && options.maxChars > 0 ? options.maxChars : DEFAULT_MAX_DETAIL_CHARS;
25
+ if (SECRET_KEY.test(key)) return '[REDACTED]';
26
+ if (depth > 6) return '[TRUNCATED_DEPTH]';
27
+ if (value == null || typeof value === 'number' || typeof value === 'boolean') return value;
28
+ if (typeof value === 'string') return redactString(value, maxChars);
29
+ if (Array.isArray(value)) return value.slice(0, 50).map((item) => redactDiagnosticValue(item, options, depth + 1));
30
+ if (typeof value === 'object') {
31
+ const result = {};
32
+ for (const [childKey, childValue] of Object.entries(value).slice(0, 80)) {
33
+ result[childKey] = redactDiagnosticValue(childValue, options, depth + 1, childKey);
34
+ }
35
+ return result;
36
+ }
37
+ return redactString(String(value), maxChars);
38
+ }
39
+
40
+ function header(headers, name) {
41
+ if (!headers) return '';
42
+ if (typeof headers.get === 'function') return clean(headers.get(name));
43
+ const wanted = name.toLowerCase();
44
+ for (const [key, value] of Object.entries(headers)) if (String(key).toLowerCase() === wanted) return clean(value);
45
+ return '';
46
+ }
47
+
48
+ function retryAfterMs(headers) {
49
+ const value = header(headers, 'retry-after');
50
+ if (!value) return null;
51
+ if (/^\d+(?:\.\d+)?$/.test(value)) return Math.max(0, Math.round(Number(value) * 1000));
52
+ const when = Date.parse(value);
53
+ return Number.isFinite(when) ? Math.max(0, when - Date.now()) : null;
54
+ }
55
+
56
+ const OPENAI_NON_RETRY_CODES = new Set([
57
+ 'credit_balance_exhausted',
58
+ 'organization_spend_limit_exceeded',
59
+ 'project_spend_limit_exceeded',
60
+ 'organization_usage_limit_exceeded',
61
+ ]);
62
+
63
+ export function classifyOpenAIError({ status, type, code, message, param } = {}) {
64
+ const httpStatus = Number(status || 0);
65
+ const normalizedType = clean(type).toLowerCase();
66
+ const normalizedCode = clean(code).toLowerCase();
67
+ const diagnosticText = `${normalizedCode} ${normalizedType} ${clean(param).toLowerCase()} ${clean(message).toLowerCase()}`;
68
+ if (normalizedCode === 'previous_response_not_found') return { category: 'continuation', retriable: true, retry_strategy: 'retry_full_context' };
69
+ if (normalizedCode === 'websocket_connection_limit_reached') return { category: 'connection_lifetime', retriable: true, retry_strategy: 'reconnect' };
70
+ if (OPENAI_NON_RETRY_CODES.has(normalizedCode)) return { category: 'billing_or_quota', retriable: false, retry_strategy: 'operator_action' };
71
+ if (httpStatus === 400 && /service[_ -]?tier/.test(diagnosticText)) return { category: 'service_tier', retriable: false, retry_strategy: 'change_configuration' };
72
+ if (httpStatus === 400) return { category: 'invalid_request', retriable: false, retry_strategy: 'change_request' };
73
+ if (httpStatus === 401) return { category: 'authentication', retriable: false, retry_strategy: 'fix_credentials' };
74
+ if (httpStatus === 403) return { category: 'authorization_or_region', retriable: false, retry_strategy: 'operator_action' };
75
+ if (httpStatus === 429 && normalizedCode === 'slow_down') return { category: 'ramp_rate', retriable: true, retry_strategy: 'retry_after_backoff' };
76
+ if (httpStatus === 429) return { category: 'rate_limit', retriable: true, retry_strategy: 'retry_after_backoff' };
77
+ if (httpStatus === 500) return { category: 'provider_server', retriable: true, retry_strategy: 'retry_backoff' };
78
+ if (httpStatus === 503 || normalizedCode === 'server_is_overloaded') return { category: 'provider_overload', retriable: true, retry_strategy: 'retry_after_backoff' };
79
+ return { category: 'provider_error', retriable: false, retry_strategy: 'inspect_error' };
80
+ }
81
+
82
+ export class AgentSamDiagnosticError extends Error {
83
+ constructor(diagnostic, options = {}) {
84
+ super(clean(diagnostic?.message) || clean(options.message) || 'AgentSam operation failed', options.cause ? { cause: options.cause } : undefined);
85
+ this.name = 'AgentSamDiagnosticError';
86
+ this.diagnostic = Object.freeze({ schema_version: 1, ...diagnostic });
87
+ this.code = diagnostic?.code || diagnostic?.kind || 'agentsam_error';
88
+ this.status = diagnostic?.http_status ?? diagnostic?.status ?? null;
89
+ }
90
+ }
91
+
92
+ export function createOpenAIHttpError({ status, body, headers, requestedServiceTier, rawText } = {}) {
93
+ const payload = body && typeof body === 'object' ? body : {};
94
+ const providerError = payload.error && typeof payload.error === 'object' ? payload.error : payload;
95
+ const type = clean(providerError.type) || null;
96
+ const code = clean(providerError.code) || null;
97
+ const message = clean(providerError.message || payload.message || rawText) || `OpenAI API returned HTTP ${status}`;
98
+ const classification = classifyOpenAIError({ status, type, code, message, param: providerError.param });
99
+ const diagnostic = {
100
+ source: 'openai',
101
+ kind: 'provider_http_error',
102
+ http_status: Number(status || 0) || null,
103
+ type,
104
+ code,
105
+ param: clean(providerError.param) || null,
106
+ message: redactString(message, 4_000),
107
+ category: classification.category,
108
+ retriable: classification.retriable,
109
+ retry_strategy: classification.retry_strategy,
110
+ retry_after_ms: retryAfterMs(headers),
111
+ request_id: header(headers, 'x-request-id') || header(headers, 'openai-request-id') || header(headers, 'request-id') || null,
112
+ ray_id: header(headers, 'cf-ray') || null,
113
+ requested_service_tier: clean(requestedServiceTier) || null,
114
+ details: redactDiagnosticValue(payload, { maxChars: DEFAULT_MAX_DETAIL_CHARS }),
115
+ };
116
+ return new AgentSamDiagnosticError(diagnostic);
117
+ }
118
+
119
+ export function createProcessDiagnosticError({ code, message, command, args, cwd, exitCode, signal, stderr, stdout, cause, retriable = false } = {}) {
120
+ return new AgentSamDiagnosticError({
121
+ source: 'process',
122
+ kind: 'process_error',
123
+ code: clean(code) || 'process_failed',
124
+ message: clean(message) || 'Process failed',
125
+ retriable: Boolean(retriable),
126
+ retry_strategy: retriable ? 'retry_after_inspection' : 'inspect_error',
127
+ command: clean(command) || null,
128
+ args: Array.isArray(args) ? args.slice(0, 64).map((value) => redactString(value, 1_000)) : [],
129
+ cwd: clean(cwd) || null,
130
+ exit_code: Number.isInteger(exitCode) ? exitCode : null,
131
+ signal: clean(signal) || null,
132
+ stderr: redactString(stderr || '', 12_000) || null,
133
+ stdout: redactString(stdout || '', 4_000) || null,
134
+ }, { cause });
135
+ }
136
+
137
+ export function diagnosticFromError(error, fallback = {}) {
138
+ if (error?.diagnostic) return error.diagnostic;
139
+ return Object.freeze({
140
+ schema_version: 1,
141
+ source: fallback.source || 'agentsam',
142
+ kind: fallback.kind || 'runtime_error',
143
+ code: clean(error?.code || fallback.code) || 'runtime_error',
144
+ message: redactString(error?.message || error || fallback.message || 'Unknown error', 4_000),
145
+ http_status: Number(error?.status || fallback.status || 0) || null,
146
+ retriable: Boolean(fallback.retriable),
147
+ retry_strategy: fallback.retry_strategy || 'inspect_error',
148
+ });
149
+ }
150
+
151
+ export function renderDiagnosticError(error) {
152
+ const d = diagnosticFromError(error);
153
+ const identity = [d.source, d.http_status ? `HTTP ${d.http_status}` : '', d.type, d.code].filter(Boolean).join(' · ');
154
+ const lines = [`✗ ${d.message}`, identity ? ` ${identity}` : ''];
155
+ if (d.request_id) lines.push(` request_id: ${d.request_id}`);
156
+ if (d.ray_id) lines.push(` ray_id: ${d.ray_id}`);
157
+ if (d.retry_after_ms != null) lines.push(` retry_after_ms: ${d.retry_after_ms}`);
158
+ if (d.retry_strategy) lines.push(` retry: ${d.retriable ? 'yes' : 'no'} · ${d.retry_strategy}`);
159
+ return lines.filter(Boolean).join('\n');
160
+ }
@@ -0,0 +1,9 @@
1
+ export {
2
+ AgentSamDiagnosticError,
3
+ classifyOpenAIError,
4
+ createOpenAIHttpError,
5
+ createProcessDiagnosticError,
6
+ diagnosticFromError,
7
+ redactDiagnosticValue,
8
+ renderDiagnosticError,
9
+ } from './diagnostic.js';