@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,42 @@
1
+ import assert from 'node:assert/strict';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import test from 'node:test';
6
+ import { createLocalSession, listLocalSessions, loadLocalSession, saveLocalSession, sessionTitleFromInput } from '../src/lib/local-sessions.js';
7
+ import { renderSessionReceipt } from '../src/commands/shell.js';
8
+
9
+ function tempHome(t) {
10
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-session-home-'));
11
+ t.after(() => fs.rmSync(home, { recursive: true, force: true }));
12
+ return home;
13
+ }
14
+
15
+ test('local sessions persist provider-neutral continuation, usage, cost, and last-input title', t => {
16
+ const home = tempHome(t);
17
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-session-project-'));
18
+ t.after(() => fs.rmSync(cwd, { recursive: true, force: true }));
19
+ let session = createLocalSession({ cwd, last_input: 'Run wrangler whoami' }, { home });
20
+ assert.match(session.id, /^asess_[0-9a-f-]{36}$/i);
21
+ session = saveLocalSession({
22
+ ...session,
23
+ title: sessionTitleFromInput('Run wrangler whoami'),
24
+ model_key: 'openai:gpt-6-astra',
25
+ actual_service_tier: 'fast',
26
+ provider_state: { provider: 'openai', previous_response_id: 'resp_123' },
27
+ usage_snapshot: { current_context: { input_tokens: 21_244, window_tokens: 1_050_000 } },
28
+ cumulative_usage: { input_tokens: 21_244, cached_input_tokens: 60_544, output_tokens: 219, reasoning_tokens: 31 },
29
+ total_cost_usd: 0.123456,
30
+ }, { home });
31
+
32
+ const loaded = loadLocalSession(session.id, { home });
33
+ assert.equal(loaded.provider_state.previous_response_id, 'resp_123');
34
+ assert.equal(loaded.cumulative_usage.cached_input_tokens, 60_544);
35
+ assert.equal(listLocalSessions({ home, cwd })[0].id, session.id);
36
+
37
+ const receipt = renderSessionReceipt(loaded);
38
+ assert.match(receipt, /Token usage: total=21,463 input=21,244 \(\+ 60,544 cached\) output=219 reasoning=31/);
39
+ assert.match(receipt, /Cost: \$0\.1235/);
40
+ assert.match(receipt, new RegExp(`agentsam resume ${session.id}`));
41
+ assert.match(receipt, /Run wrangler whoami/);
42
+ });
@@ -0,0 +1,83 @@
1
+ import assert from 'node:assert/strict';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import fs from 'node:fs';
5
+ import { describe, it } from 'node:test';
6
+ import { fileURLToPath } from 'node:url';
7
+ import {
8
+ DEPLOY_INPUT_GLOBS,
9
+ loadOptionalCloudflareEnv,
10
+ resolveLocalStudioDeployable,
11
+ wranglerDeployCommand,
12
+ isLocalStudioCheckout,
13
+ runLocalStudioDeploy,
14
+ } from '../src/lib/deploy/local-studio.js';
15
+ import { productionDeployBlockedReason } from '../src/lib/deploy/git-guard.js';
16
+
17
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
18
+
19
+ describe('local-studio deploy resolver', () => {
20
+ it('resolves apps/local-studio + backend/wrangler.jsonc from the repo', () => {
21
+ const target = resolveLocalStudioDeployable(root);
22
+ assert.equal(target.provider, 'cloudflare');
23
+ assert.equal(target.app, 'local-studio');
24
+ assert.equal(path.basename(path.dirname(target.wranglerConfig)), 'backend');
25
+ assert.deepEqual(target.wranglerArgs, ['deploy', '-c', 'backend/wrangler.jsonc']);
26
+ assert.equal(isLocalStudioCheckout(root), true);
27
+ const cmd = wranglerDeployCommand(target, { dryRun: true });
28
+ assert.equal(cmd.bin, 'npx');
29
+ assert.equal(cmd.cwd, target.appRoot);
30
+ assert.deepEqual(cmd.args, ['wrangler', 'deploy', '-c', 'backend/wrangler.jsonc', '--dry-run']);
31
+ });
32
+
33
+ it('fingerprints the cloudflare connector package', () => {
34
+ assert.equal(DEPLOY_INPUT_GLOBS.includes('packages/connectors/cloudflare'), true);
35
+ assert.equal(DEPLOY_INPUT_GLOBS.includes('apps/local-studio'), true);
36
+ });
37
+
38
+ it('treats missing .env.cloudflare as optional', () => {
39
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cf-env-'));
40
+ const loaded = loadOptionalCloudflareEnv(tmp);
41
+ assert.equal(loaded.loaded, false);
42
+ assert.deepEqual(loaded.vars, {});
43
+ });
44
+
45
+ it('plan does not invoke wrangler', async () => {
46
+ const result = await runLocalStudioDeploy({ cwd: root, planOnly: true, execute: false });
47
+ assert.equal(result.plan.provider, 'cloudflare');
48
+ assert.equal(result.plan.wranglerConfig, 'apps/local-studio/backend/wrangler.jsonc');
49
+ assert.equal(result.plan.genericRootDeploy, false);
50
+ assert.equal(result.receipt.plan, true);
51
+ assert.equal(result.receipt.promoted, false);
52
+ assert.equal(result.receipt.wranglerConfig, 'apps/local-studio/backend/wrangler.jsonc');
53
+ assert.equal(result.receipt.hostname, 'agentsam.inneranimalmedia.com');
54
+ });
55
+
56
+ it('blocks production deploys from feat branches', () => {
57
+ const reason = productionDeployBlockedReason({
58
+ ok: true,
59
+ branch: 'feat/cloudflare-connector-deploy-control',
60
+ detached: false,
61
+ head: 'aaa',
62
+ originMain: 'aaa',
63
+ dirty: false,
64
+ tmpCheckout: false,
65
+ repoRoot: root,
66
+ });
67
+ assert.match(reason, /feat\/cloudflare-connector-deploy-control/);
68
+ });
69
+
70
+ it('allows production only when HEAD is a clean origin/main', () => {
71
+ const reason = productionDeployBlockedReason({
72
+ ok: true,
73
+ branch: 'main',
74
+ detached: false,
75
+ head: 'abc',
76
+ originMain: 'abc',
77
+ dirty: false,
78
+ tmpCheckout: false,
79
+ repoRoot: root,
80
+ });
81
+ assert.equal(reason, null);
82
+ });
83
+ });
@@ -0,0 +1,43 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { calculateModelCost, getModelRecord } from '../src/models/index.js';
4
+
5
+ test('Astra catalog separates technical window, reasoning controls, service tiers, and economics', () => {
6
+ const model = getModelRecord('gpt-6-astra');
7
+ assert.ok(model);
8
+ assert.equal(model.context_window, 1_050_000);
9
+ assert.equal(model.max_output_tokens, 128_000);
10
+ assert.deepEqual(model.reasoning_efforts, ['low', 'medium', 'high', 'xhigh', 'max']);
11
+ assert.deepEqual(model.service_tiers, ['default', 'fast', 'flex']);
12
+ assert.equal(model.context_policy.target_input_tokens, 120_000);
13
+ assert.equal(model.context_policy.compact_at_tokens, 180_000);
14
+ assert.equal(model.context_policy.max_normal_input_tokens, 250_000);
15
+ assert.equal(model.context_policy.pricing_threshold_tokens, 272_000);
16
+ assert.equal(model.batch.interactive, false);
17
+ });
18
+
19
+ test('cost calculation applies the long-context threshold to the full request only after crossing it', () => {
20
+ const atBoundary = calculateModelCost('gpt-6-astra', { input_tokens: 272_000, output_tokens: 10_000 });
21
+ assert.equal(atBoundary.threshold_applied, null);
22
+ assert.equal(atBoundary.rates_per_million.input, 10);
23
+ assert.equal(atBoundary.rates_per_million.output, 50);
24
+
25
+ const crossed = calculateModelCost('gpt-6-astra', { input_tokens: 272_001, output_tokens: 10_000 });
26
+ assert.equal(crossed.threshold_applied.input_tokens_gt, 272_000);
27
+ assert.equal(crossed.rates_per_million.input, 20);
28
+ assert.equal(crossed.rates_per_million.output, 75);
29
+ assert.ok(crossed.total_usd > atBoundary.total_usd);
30
+ });
31
+
32
+ test('service tier and cache pricing stay explicit rather than hidden in prompts', () => {
33
+ const usage = { input_tokens: 100_000, cached_input_tokens: 80_000, cache_write_tokens: 10_000, output_tokens: 20_000 };
34
+ const standard = calculateModelCost('gpt-6-astra', usage, { serviceTier: 'default' });
35
+ const fast = calculateModelCost('gpt-6-astra', usage, { serviceTier: 'fast' });
36
+ const flex = calculateModelCost('gpt-6-astra', usage, { serviceTier: 'flex' });
37
+ const batch = calculateModelCost('gpt-6-astra', usage, { serviceTier: 'batch' });
38
+ assert.equal(fast.total_usd, standard.total_usd * 2);
39
+ assert.equal(flex.total_usd, standard.total_usd * 0.5);
40
+ assert.equal(batch.total_usd, standard.total_usd * 0.5);
41
+ assert.equal(standard.rates_per_million.cached_input, 1);
42
+ assert.equal(standard.rates_per_million.cache_write, 12.5);
43
+ });
@@ -1,37 +1,51 @@
1
1
  import assert from 'node:assert/strict';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
2
5
  import test from 'node:test';
3
-
4
6
  import { collectModelsStatus, renderModelsStatus } from '../src/commands/models.js';
5
7
 
6
8
  function response(body, status = 200) {
7
- return {
8
- ok: status >= 200 && status < 300,
9
- status,
10
- async json() { return body; },
11
- };
9
+ return { ok: status >= 200 && status < 300, status, async json() { return body; } };
10
+ }
11
+
12
+ function tempHome(t) {
13
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-model-home-'));
14
+ t.after(() => fs.rmSync(home, { recursive: true, force: true }));
15
+ return home;
12
16
  }
13
17
 
14
- test('model inventory reports configured API providers without exposing credentials', async () => {
18
+ test('model inventory reports configured API providers without exposing credentials', async t => {
19
+ const home = tempHome(t);
15
20
  const status = await collectModelsStatus({
21
+ home,
22
+ discoverRemote: false,
16
23
  env: {
17
- OPENAI_API_KEY: 'secret-openai',
18
- GEMINI_API_KEY: '',
19
- XAI_API_KEY: 'secret-xai',
20
- OLLAMA_BASE_URL: 'http://127.0.0.1:11434',
21
- OLLAMA_MODEL: 'qwen:test',
22
- OLLAMA_EMBED_MODEL: 'embed:test',
24
+ OPENAI_API_KEY: 'secret-openai', GEMINI_API_KEY: '', XAI_API_KEY: 'secret-xai', ANTHROPIC_API_KEY: 'secret-anthropic',
25
+ OLLAMA_BASE_URL: 'http://127.0.0.1:11434', OLLAMA_MODEL: 'qwen:test', OLLAMA_EMBED_MODEL: 'embed:test',
23
26
  },
24
27
  fetchImpl: async () => response({ models: [{ name: 'qwen:test' }, { name: 'embed:test' }] }),
25
28
  });
26
-
27
29
  assert.equal(status.providers.find((row) => row.id === 'openai').configured, true);
28
30
  assert.equal(status.providers.find((row) => row.id === 'gemini').configured, false);
29
31
  assert.equal(status.providers.find((row) => row.id === 'grok').configured, true);
32
+ assert.equal(status.providers.find((row) => row.id === 'anthropic').configured, true);
30
33
  assert.equal(status.local.online, true);
31
34
  assert.deepEqual(status.local.models.map((row) => row.name), ['qwen:test', 'embed:test']);
32
-
33
35
  const rendered = renderModelsStatus(status);
34
36
  assert.match(rendered, /OpenAI/);
35
37
  assert.match(rendered, /qwen:test/);
36
- assert.doesNotMatch(rendered, /secret-openai|secret-xai/);
38
+ assert.doesNotMatch(rendered, /secret-openai|secret-xai|secret-anthropic/);
39
+ });
40
+
41
+ test('exact hosted model availability is verified against the provider inventory', async () => {
42
+ const status = await collectModelsStatus({
43
+ env: { OPENAI_API_KEY: 'secret-openai', OLLAMA_BASE_URL: 'http://127.0.0.1:11434' },
44
+ fetchImpl: async () => response({ models: [] }),
45
+ providerFetchImpl: async () => response({ data: [{ id: 'gpt-6-astra' }, { id: 'some-other-model' }] }),
46
+ });
47
+ assert.equal(status.discovery.openai.ok, true);
48
+ assert.equal(status.discovery.openai.returnedModelCount, 2);
49
+ assert.deepEqual(status.availableModels.map((row) => row.provider_model_id), ['gpt-6-astra']);
50
+ assert.equal(status.catalogModels[0].availability, 'available');
37
51
  });
@@ -0,0 +1,29 @@
1
+ import assert from 'node:assert/strict';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { describe, it } from 'node:test';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
9
+ const lockPath = path.join(root, 'apps/local-studio/package-lock.json');
10
+
11
+ describe('local-studio npm 10 lockfile', () => {
12
+ it('pins lru-cache@11.5.2 so Cloudflare npm ci can resolve it', () => {
13
+ const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
14
+ assert.equal(lock.lockfileVersion, 3);
15
+ const pkgs = lock.packages || {};
16
+ const hits = Object.entries(pkgs).filter(([key, meta]) => key.includes('lru-cache') && meta.version === '11.5.2');
17
+ assert.ok(hits.length >= 1, 'Missing: lru-cache@11.5.2 from lock file');
18
+ const [, meta] = hits[0];
19
+ assert.match(meta.resolved || '', /lru-cache-11\.5\.2\.tgz/);
20
+ assert.ok(meta.integrity);
21
+ });
22
+
23
+ it('verify-npm10-lock script exits 0 against the current lock', () => {
24
+ const script = path.join(root, 'apps/local-studio/scripts/verify-npm10-lock.mjs');
25
+ const r = spawnSync(process.execPath, [script], { encoding: 'utf8' });
26
+ assert.equal(r.status, 0, r.stderr || r.stdout);
27
+ assert.match(r.stdout, /lru-cache@11\.5\.2/);
28
+ });
29
+ });
@@ -0,0 +1,95 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { createOpenAIResponsesAdapter } from '../src/providers/index.js';
4
+
5
+ function jsonResponse(body, status = 200) {
6
+ return { ok: status >= 200 && status < 300, status, async json() { return body; } };
7
+ }
8
+
9
+ test('Responses adapter sends reasoning/service tier without legacy sampling parameters and reconciles provider usage', async () => {
10
+ let request = null;
11
+ const events = [];
12
+ const adapter = createOpenAIResponsesAdapter({
13
+ apiKey: 'test-key',
14
+ emit: event => events.push(event),
15
+ fetchImpl: async (url, options) => {
16
+ request = { url, options, body: JSON.parse(options.body) };
17
+ return jsonResponse({
18
+ id: 'resp_test', status: 'completed', model: 'gpt-6-astra', service_tier: 'fast',
19
+ output: [{ type: 'message', content: [{ type: 'output_text', text: 'done' }] }],
20
+ usage: {
21
+ input_tokens: 100_000,
22
+ input_tokens_details: { cached_tokens: 80_000, cache_write_tokens: 10_000 },
23
+ output_tokens: 5_000,
24
+ output_tokens_details: { reasoning_tokens: 2_000 },
25
+ },
26
+ });
27
+ },
28
+ });
29
+ const result = await adapter.create({ model: 'gpt-6-astra', input: 'work', reasoningEffort: 'high', serviceTier: 'fast' });
30
+ assert.equal(request.url, 'https://api.openai.com/v1/responses');
31
+ assert.equal(request.body.reasoning.effort, 'high');
32
+ assert.equal(request.body.service_tier, 'fast');
33
+ assert.equal(request.body.truncation, 'disabled');
34
+ assert.equal('temperature' in request.body, false);
35
+ assert.equal('top_p' in request.body, false);
36
+ assert.equal(result.output_text, 'done');
37
+ assert.equal(result.usage_snapshot.current_context.input_tokens, 100_000);
38
+ assert.equal(result.usage_snapshot.current_context.window_tokens, 1_050_000);
39
+ assert.equal(result.actual_service_tier, 'fast');
40
+ assert.equal(result.cost.rates_per_million.input, 20);
41
+ assert.deepEqual(events.map(event => event.type), ['model.started', 'usage.snapshot', 'cost.snapshot', 'model.completed']);
42
+ });
43
+
44
+ test('Responses adapter preserves function call_id when returning tool output', async () => {
45
+ const bodies = [];
46
+ const adapter = createOpenAIResponsesAdapter({
47
+ apiKey: 'test-key',
48
+ fetchImpl: async (_url, options) => {
49
+ const body = JSON.parse(options.body);
50
+ bodies.push(body);
51
+ if (bodies.length === 1) {
52
+ return jsonResponse({
53
+ id: 'resp_calls', status: 'completed', service_tier: 'default',
54
+ output: [{ type: 'function_call', id: 'fc_1', call_id: 'call_abc', name: 'repository_snapshot', arguments: '{"depth":"quick"}', status: 'completed' }],
55
+ usage: { input_tokens: 100, output_tokens: 20 },
56
+ });
57
+ }
58
+ return jsonResponse({
59
+ id: 'resp_done', status: 'completed', service_tier: 'default',
60
+ output: [{ type: 'message', content: [{ type: 'output_text', text: 'complete' }] }],
61
+ usage: { input_tokens: 200, output_tokens: 30 },
62
+ });
63
+ },
64
+ });
65
+ const first = await adapter.create({
66
+ model: 'gpt-6-astra', input: 'inspect', reasoningEffort: 'low', serviceTier: 'default',
67
+ tools: [{ type: 'function', name: 'repository_snapshot', description: 'Snapshot repo', parameters: { type: 'object', properties: {} } }],
68
+ });
69
+ assert.equal(first.tool_calls[0].call_id, 'call_abc');
70
+ const second = await adapter.continueWithToolOutputs({
71
+ model: 'gpt-6-astra', previousResponseId: first.response_id, reasoningEffort: 'low', serviceTier: 'default',
72
+ toolOutputs: [{ call_id: first.tool_calls[0].call_id, output: { ok: true } }],
73
+ });
74
+ assert.equal(bodies[1].previous_response_id, 'resp_calls');
75
+ assert.deepEqual(bodies[1].input, [{ type: 'function_call_output', call_id: 'call_abc', output: '{"ok":true}' }]);
76
+ assert.equal(second.output_text, 'complete');
77
+ });
78
+
79
+ test('compaction is an explicit provider operation and has no arbitrary default wall-clock timeout', async () => {
80
+ let signal = 'unset';
81
+ let body = null;
82
+ const adapter = createOpenAIResponsesAdapter({
83
+ apiKey: 'test-key',
84
+ fetchImpl: async (url, options) => {
85
+ assert.equal(url, 'https://api.openai.com/v1/responses/compact');
86
+ signal = options.signal;
87
+ body = JSON.parse(options.body);
88
+ return jsonResponse({ id: 'cmp_1', object: 'response.compaction', output: [{ type: 'compaction', encrypted_content: 'opaque' }], usage: { input_tokens: 1_000, output_tokens: 100 } });
89
+ },
90
+ });
91
+ const compacted = await adapter.compact({ model: 'gpt-6-astra', previousResponseId: 'resp_previous' });
92
+ assert.equal(signal, undefined);
93
+ assert.equal(body.previous_response_id, 'resp_previous');
94
+ assert.equal(compacted.compaction_id, 'cmp_1');
95
+ });
@@ -0,0 +1,52 @@
1
+ import assert from 'node:assert/strict';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import test from 'node:test';
6
+ import { describeProviderCredential, resolveProviderCredential } from '../src/lib/provider-credentials.js';
7
+
8
+ function fixtureHome(t) {
9
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-credentials-'));
10
+ t.after(() => fs.rmSync(home, { recursive: true, force: true }));
11
+ fs.mkdirSync(path.join(home, '.agentsam', 'env.d'), { recursive: true });
12
+ return home;
13
+ }
14
+
15
+ test('provider credential resolver loads a secure AgentSam env file without exposing it in status', t => {
16
+ const home = fixtureHome(t);
17
+ const filename = path.join(home, '.agentsam', 'env.d', 'openai.env');
18
+ fs.writeFileSync(filename, 'export OPENAI_API_KEY="secret-from-file"\n', { mode: 0o600 });
19
+ if (process.platform !== 'win32') fs.chmodSync(filename, 0o600);
20
+
21
+ const resolved = resolveProviderCredential('openai', { env: {}, home });
22
+ assert.equal(resolved.configured, true);
23
+ assert.equal(resolved.source, 'agentsam_env_file');
24
+ assert.equal(resolved.value, 'secret-from-file');
25
+
26
+ const safe = describeProviderCredential('openai', { env: {}, home });
27
+ assert.equal(safe.configured, true);
28
+ assert.equal(safe.source, 'agentsam_env_file');
29
+ assert.equal(Object.hasOwn(safe, 'value'), false);
30
+ assert.doesNotMatch(JSON.stringify(safe), /secret-from-file/);
31
+ });
32
+
33
+ test('environment credential wins over disk fallback', t => {
34
+ const home = fixtureHome(t);
35
+ const filename = path.join(home, '.agentsam', 'env.d', 'openai.env');
36
+ fs.writeFileSync(filename, 'OPENAI_API_KEY=file-value\n', { mode: 0o600 });
37
+ if (process.platform !== 'win32') fs.chmodSync(filename, 0o600);
38
+ const resolved = resolveProviderCredential('openai', { env: { OPENAI_API_KEY: 'env-value' }, home });
39
+ assert.equal(resolved.source, 'environment');
40
+ assert.equal(resolved.value, 'env-value');
41
+ });
42
+
43
+ test('AgentSam refuses provider credential files with broad POSIX permissions', { skip: process.platform === 'win32' }, t => {
44
+ const home = fixtureHome(t);
45
+ const filename = path.join(home, '.agentsam', 'env.d', 'openai.env');
46
+ fs.writeFileSync(filename, 'OPENAI_API_KEY=unsafe-value\n', { mode: 0o644 });
47
+ fs.chmodSync(filename, 0o644);
48
+ const resolved = resolveProviderCredential('openai', { env: {}, home });
49
+ assert.equal(resolved.configured, false);
50
+ assert.equal(resolved.error, 'permissions_too_open');
51
+ assert.equal(resolved.value, '');
52
+ });
@@ -0,0 +1,25 @@
1
+ import assert from 'node:assert/strict';
2
+ import { createHash } from 'node:crypto';
3
+ import test from 'node:test';
4
+ import { compactConsumedToolResult, rehydrateContextRef } from '../src/context/index.js';
5
+
6
+ const sha = value => `sha256:${createHash('sha256').update(value).digest('hex')}`;
7
+
8
+ test('large consumed evidence can leave active context and later rehydrate by stable ref/hash', async () => {
9
+ const source = 'important evidence '.repeat(800);
10
+ const ref = 'tool:call_123';
11
+ const digest = sha(source);
12
+ const compacted = compactConsumedToolResult(source, { ref, hash: digest });
13
+ assert.equal(compacted.compacted, true);
14
+ assert.equal(compacted.chars, 4_000);
15
+ assert.equal(compacted.source_chars, source.length);
16
+ const adapter = { async read(requested) { assert.equal(requested, ref); return { content: source, hash: digest, kind: 'tool_result' }; } };
17
+ const restored = await rehydrateContextRef(ref, adapter, { expectedHash: digest, kind: 'tool_result', maxChars: 64_000 });
18
+ assert.equal(restored.content, source);
19
+ assert.equal(restored.hash, digest);
20
+ assert.equal(restored.truncated, false);
21
+ });
22
+
23
+ test('rehydration fails closed when stable source hash changed', async () => {
24
+ await assert.rejects(() => rehydrateContextRef('file:x', async () => ({ content: 'changed' }), { expectedHash: sha('original') }), /rehydration_hash_mismatch/);
25
+ });
@@ -6,15 +6,15 @@ import test from 'node:test';
6
6
  const root = path.resolve(import.meta.dirname, '..');
7
7
  const read = (rel) => fs.readFileSync(path.join(root, rel), 'utf8');
8
8
 
9
- test('2.5 release metadata and public terminal vocabulary are aligned', () => {
9
+ test('2.6 release metadata and public terminal vocabulary are aligned', () => {
10
10
  const pkg = JSON.parse(read('package.json'));
11
11
  const manifest = read('agentsam.yaml');
12
12
  const readme = read('README.md');
13
13
 
14
- assert.equal(pkg.version, '2.5.0');
15
- assert.match(manifest, /target_version: "2\.5\.0"/);
14
+ assert.equal(pkg.version, '2.6.0');
15
+ assert.match(manifest, /target_version: "2.6.0"/);
16
16
  assert.match(manifest, /state: release_candidate/);
17
- assert.match(manifest, /current_latest: "2\.4\.1"/);
17
+ assert.match(manifest, /current_latest: "2.5.0"/);
18
18
  assert.doesNotMatch(manifest, /^\s*- tui\s*$/m);
19
19
  assert.doesNotMatch(readme, /agentsam tui|CLI\/TUI/);
20
20
  });
@@ -0,0 +1,148 @@
1
+ import assert from 'node:assert/strict';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import test from 'node:test';
6
+ import { createCapabilityAdapter } from '../src/agent/capability-adapter.js';
7
+ import { buildAgentToolSurface, capabilityFunctionName, runResponsesAgent } from '../src/agent/responses-runner.js';
8
+
9
+ function usage(input = 10_000, cumulative = input) {
10
+ return {
11
+ current_context: { input_tokens: input, window_tokens: 1_050_000 },
12
+ cumulative: { input_tokens: cumulative, output_tokens: 100, cached_input_tokens: 0, cache_write_tokens: 0, reasoning_tokens: 0 },
13
+ estimate_kind: 'provider', provider_authoritative: true,
14
+ };
15
+ }
16
+ function cost(total = 0.1) { return { total_usd: total }; }
17
+
18
+ test('capability adapter hydrates packaged JSON schemas and tool surface exposes only selected executable schemas', () => {
19
+ const adapter = createCapabilityAdapter();
20
+ const descriptors = adapter.toolDescriptors();
21
+ assert.deepEqual(descriptors.map((row) => row.name).sort(), ['cloudflare.cpu.profile', 'cloudflare.wrangler.native', 'repository.snapshot']);
22
+ const repository = descriptors.find((row) => row.name === 'repository.snapshot');
23
+ assert.equal(repository.input_schema.type, 'object');
24
+ assert.ok(repository.input_schema.properties.cwd);
25
+ const surface = buildAgentToolSurface(adapter, 'snapshot inspect repository');
26
+ assert.equal(surface.tools.length, 1);
27
+ assert.equal(surface.tools[0].name, capabilityFunctionName('repository.snapshot'));
28
+ assert.equal(surface.tools[0].parameters.type, 'object');
29
+ assert.equal(surface.receipt.catalog_tools, 3);
30
+ });
31
+
32
+ test('runner owns cwd, executes selected tool, preserves call_id and returns provider-authoritative continuation', async t => {
33
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-runner-'));
34
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
35
+ fs.mkdirSync(path.join(root, '.git'));
36
+ fs.writeFileSync(path.join(root, 'package.json'), '{"name":"runner-demo"}');
37
+ let invoked = null;
38
+ const capabilityAdapter = createCapabilityAdapter({
39
+ handlers: {
40
+ 'repository.snapshot': async input => {
41
+ invoked = input;
42
+ return { ok: true, cwd: input.cwd, evidence: 'x'.repeat(500) };
43
+ },
44
+ },
45
+ });
46
+ const provider = {
47
+ async create(params) {
48
+ assert.equal(params.model, 'gpt-6-astra');
49
+ assert.equal(params.reasoningEffort, 'high');
50
+ assert.equal(params.serviceTier, 'fast');
51
+ assert.equal(params.tools.length, 1);
52
+ return {
53
+ response_id: 'resp_1', output_text: '', actual_service_tier: 'fast',
54
+ tool_calls: [{ call_id: 'call_1', name: capabilityFunctionName('repository.snapshot'), arguments: '{"cwd":"/tmp/attacker","churnDays":7}' }],
55
+ usage_snapshot: usage(20_000), cost: cost(0.2),
56
+ };
57
+ },
58
+ async continueWithToolOutputs(params) {
59
+ assert.equal(params.previousResponseId, 'resp_1');
60
+ assert.equal(params.toolOutputs[0].call_id, 'call_1');
61
+ assert.match(params.toolOutputs[0].output, /runner-demo|runner-/);
62
+ return { response_id: 'resp_2', output_text: 'done', actual_service_tier: 'fast', tool_calls: [], usage_snapshot: usage(21_000, 41_000), cost: cost(0.3) };
63
+ },
64
+ async compact() { throw new Error('should_not_compact'); },
65
+ };
66
+ const events = [];
67
+ const result = await runResponsesAgent({
68
+ provider, capabilityAdapter, cwd: root, prompt: 'snapshot inspect repository', model: 'gpt-6-astra', reasoningEffort: 'high', serviceTier: 'fast',
69
+ emit: event => events.push(event),
70
+ });
71
+ assert.equal(invoked.cwd, root);
72
+ assert.equal(invoked.churnDays, 7);
73
+ assert.equal(result.output_text, 'done');
74
+ assert.equal(result.response_id, 'resp_2');
75
+ assert.equal(result.total_cost_usd, 0.5);
76
+ assert.equal(result.continuation.compact_before_next_turn, false);
77
+ assert.ok(events.some(event => event.type === 'tool.search'));
78
+ assert.ok(events.some(event => event.type === 'tool.completed'));
79
+ });
80
+
81
+ test('runtime approval hook can deny a model-triggered tool before execution', async t => {
82
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-runner-denied-'));
83
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
84
+ fs.writeFileSync(path.join(root, 'package.json'), '{"name":"denied-demo"}');
85
+ let invoked = false;
86
+ const capabilityAdapter = createCapabilityAdapter({
87
+ handlers: {
88
+ 'repository.snapshot': async () => { invoked = true; return { ok: true }; },
89
+ },
90
+ });
91
+ const provider = {
92
+ async create() {
93
+ return {
94
+ response_id: 'resp_deny', output_text: '', actual_service_tier: 'default',
95
+ tool_calls: [{ call_id: 'call_deny', name: capabilityFunctionName('repository.snapshot'), arguments: '{}' }],
96
+ usage_snapshot: usage(10_000), cost: cost(0.01),
97
+ };
98
+ },
99
+ async continueWithToolOutputs() { throw new Error('continuation_should_not_run'); },
100
+ };
101
+ await assert.rejects(() => runResponsesAgent({
102
+ provider, capabilityAdapter, cwd: root, prompt: 'snapshot repository', model: 'gpt-6-astra', reasoningEffort: 'low', serviceTier: 'default',
103
+ beforeTool: async request => {
104
+ assert.equal(request.capability_id, 'repository.snapshot');
105
+ assert.equal(request.cwd, root);
106
+ return false;
107
+ },
108
+ }), /tool_execution_not_approved:repository\.snapshot/);
109
+ assert.equal(invoked, false);
110
+ });
111
+
112
+ test('runner compacts before a projected high-context continuation rather than crossing normal policy blindly', async () => {
113
+ let compactCalls = 0;
114
+ let createInput = null;
115
+ const capabilityAdapter = createCapabilityAdapter();
116
+ const provider = {
117
+ async compact(params) {
118
+ compactCalls += 1;
119
+ assert.equal(params.previousResponseId, 'resp_old');
120
+ return { output: [{ type: 'compaction', encrypted_content: 'opaque-small' }] };
121
+ },
122
+ async create(params) {
123
+ createInput = params.input;
124
+ assert.equal(params.previousResponseId, undefined);
125
+ return { response_id: 'resp_new', output_text: 'ok', actual_service_tier: 'default', tool_calls: [], usage_snapshot: usage(30_000), cost: cost(0.1) };
126
+ },
127
+ async continueWithToolOutputs() { throw new Error('unused'); },
128
+ };
129
+ const result = await runResponsesAgent({
130
+ provider, capabilityAdapter, cwd: process.cwd(), prompt: 'continue repository work', model: 'gpt-6-astra', reasoningEffort: 'low', serviceTier: 'default',
131
+ previousResponseId: 'resp_old', previousUsageSnapshot: usage(179_500),
132
+ });
133
+ assert.equal(compactCalls, 1);
134
+ assert.ok(Array.isArray(createInput));
135
+ assert.equal(createInput[0].type, 'compaction');
136
+ assert.equal(result.compacted_before_turn, true);
137
+ });
138
+
139
+ test('runner refuses an oversized initial context and supports an explicit projected call-cost ceiling', async () => {
140
+ const capabilityAdapter = createCapabilityAdapter();
141
+ const provider = { async create() { throw new Error('provider_should_not_run'); }, async continueWithToolOutputs() {}, async compact() {} };
142
+ await assert.rejects(() => runResponsesAgent({
143
+ provider, capabilityAdapter, cwd: process.cwd(), prompt: 'x'.repeat(800_000), model: 'gpt-6-astra', reasoningEffort: 'low', serviceTier: 'default',
144
+ }), /context_preflight_(pricing_threshold|max_normal|compaction_required)/);
145
+ await assert.rejects(() => runResponsesAgent({
146
+ provider, capabilityAdapter, cwd: process.cwd(), prompt: 'small task', model: 'gpt-6-astra', reasoningEffort: 'low', serviceTier: 'fast', maxCallCostUsd: 0.01,
147
+ }), /projected_call_cost_exceeds_budget/);
148
+ });