@inneranimalmedia/agentsam-sdk 2.5.0 → 2.6.1

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 (211) 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/PLATFORM_RUNTIME_EVENTS.md +48 -0
  7. package/docs/RELEASES.md +16 -7
  8. package/docs/SOURCE_ARCHITECTURE.md +58 -0
  9. package/docs/TEST_TIERS.md +26 -0
  10. package/migrations/runtime/0001_cli_runtime.sql +298 -0
  11. package/package.json +45 -12
  12. package/packages/agentsam-repository/README.md +15 -0
  13. package/packages/agentsam-repository/package.json +25 -0
  14. package/packages/agentsam-repository/src/contracts.js +113 -0
  15. package/packages/agentsam-repository/src/index.js +3 -0
  16. package/{src/lib → packages/agentsam-repository/src}/merkle/cloudflare-persistence.js +14 -24
  17. package/{src/lib → packages/agentsam-repository/src}/merkle/index.js +1 -0
  18. package/{src/lib → packages/agentsam-repository/src}/merkle/persistence.js +6 -4
  19. package/{src/lib → packages/agentsam-repository/src}/merkle/policy.js +1 -0
  20. package/packages/agentsam-repository/test/contracts.test.mjs +40 -0
  21. package/packages/agentsam-repository/test/git-context.test.mjs +24 -0
  22. package/{test/merkle.test.mjs → packages/agentsam-repository/test/merkle-core.test.mjs} +2 -32
  23. package/{test → packages/agentsam-repository/test}/merkle-persistence.test.mjs +11 -6
  24. package/packages/connectors/cloudflare/package.json +10 -0
  25. package/packages/connectors/cloudflare/src/index.js +127 -0
  26. package/packages/connectors/cloudflare/src/owner.js +76 -0
  27. package/packages/connectors/cloudflare/src/routes.js +223 -0
  28. package/packages/connectors/cloudflare/src/vault.js +80 -0
  29. package/packages/connectors/cloudflare/tests/connector.test.mjs +44 -0
  30. package/packages/identity/package.json +2 -2
  31. package/packages/identity/src/contracts/auth-config.js +18 -7
  32. package/packages/identity/tests/auth-config.test.mjs +9 -5
  33. package/packages/identity/tests/oauth-credentials.test.mjs +4 -4
  34. package/protocol/COMPANY_REPOSITORY_GRAPH_V1.md +91 -0
  35. package/protocol/MERKLE_PERSISTENCE_V1.md +2 -0
  36. package/protocol/MERKLE_PERSISTENCE_V2.md +40 -0
  37. package/protocol/README.md +1 -0
  38. package/protocol/capabilities/cloudflare-cpu-audit-input.schema.json +19 -0
  39. package/protocol/capabilities/cloudflare-cpu-profile-input.schema.json +13 -0
  40. package/protocol/capabilities/cloudflare-wrangler-native-input.schema.json +19 -0
  41. package/protocol/capabilities/manifest.json +47 -0
  42. package/protocol/context/context-budget.schema.json +10 -15
  43. package/protocol/context/context-item.schema.json +4 -5
  44. package/protocol/context/resolved-context-pack.schema.json +19 -14
  45. package/protocol/models/README.md +373 -0
  46. package/protocol/models/model-inventory-v2.schema.json +212 -0
  47. package/protocol/repository/repository-contract.schema.json +24 -0
  48. package/protocol/repository/repository-dependency.schema.json +24 -0
  49. package/protocol/repository/repository-identity.schema.json +17 -0
  50. package/protocol/rpc/v1/common.proto +16 -0
  51. package/protocol/rpc/v1/errors.proto +35 -0
  52. package/protocol/rpc/v1/knowledge.proto +77 -0
  53. package/services/knowledge/package-lock.json +333 -0
  54. package/services/knowledge/package.json +5 -1
  55. package/skills/agentsam-cloudflare-workers/SKILL.md +53 -0
  56. package/skills/agentsam-cloudflare-workers/references/cpu-profiling.md +16 -0
  57. package/skills/agentsam-cloudflare-workers/references/errors-and-observability.md +29 -0
  58. package/skills/agentsam-cloudflare-workers/references/wrangler-native-map.md +28 -0
  59. package/skills/catalog.json +18 -0
  60. package/src/agent/capability-adapter.js +25 -13
  61. package/src/agent/index.js +1 -0
  62. package/src/agent/responses-runner.js +353 -0
  63. package/src/capabilities/repository-snapshot.js +3 -3
  64. package/src/cli.js +118 -31
  65. package/src/cloudflare/cpu-profile.js +115 -0
  66. package/src/cloudflare/index.js +14 -0
  67. package/src/cloudflare/wrangler.js +132 -0
  68. package/src/commands/account-auth.js +47 -0
  69. package/src/commands/cloudflare.js +58 -0
  70. package/src/commands/connections.js +93 -0
  71. package/src/commands/context-economics.js +129 -0
  72. package/src/commands/context.js +1 -1
  73. package/src/commands/db.js +20 -3
  74. package/src/commands/deploy.js +39 -3
  75. package/src/commands/env.js +90 -0
  76. package/src/commands/eval.js +63 -0
  77. package/src/commands/interactive.js +2 -5
  78. package/src/commands/knowledge.js +12 -4
  79. package/src/commands/merkle-persist.js +30 -11
  80. package/src/commands/merkle.js +1 -1
  81. package/src/commands/models.js +149 -46
  82. package/src/commands/ollama.js +26 -0
  83. package/src/commands/preferences.js +130 -61
  84. package/src/commands/resume.js +67 -0
  85. package/src/commands/security.js +5 -3
  86. package/src/commands/shell.js +568 -119
  87. package/src/commands/tunnel.js +2 -2
  88. package/src/commands/whoami.js +86 -0
  89. package/src/context/budget.js +68 -6
  90. package/src/context/index.js +3 -1
  91. package/src/context/rehydrate.js +35 -0
  92. package/src/context/resolve.js +44 -12
  93. package/src/errors/contract.js +236 -0
  94. package/src/errors/diagnostic.js +160 -0
  95. package/src/errors/index.js +23 -0
  96. package/src/eval/context.js +191 -0
  97. package/src/eval/index.js +1 -0
  98. package/src/index.js +68 -2
  99. package/src/knowledge/service/auth.js +13 -0
  100. package/src/knowledge/service/grpc-client.js +115 -0
  101. package/src/knowledge/service/grpc-codec.js +237 -0
  102. package/src/knowledge/service/grpc-server.js +83 -0
  103. package/src/knowledge/service/job-engine.js +248 -0
  104. package/src/knowledge/service/server.js +87 -135
  105. package/src/knowledge/source.js +1 -1
  106. package/src/lib/account-session.js +98 -0
  107. package/src/lib/agent-instructions.js +73 -0
  108. package/src/lib/auth.js +4 -0
  109. package/src/lib/cli-preferences.js +55 -24
  110. package/src/lib/deploy/git-guard.js +69 -0
  111. package/src/lib/deploy/health.js +57 -0
  112. package/src/lib/deploy/local-studio.js +283 -0
  113. package/src/lib/deploy/secret-scan.js +65 -0
  114. package/src/lib/deploy-receipt/index.js +2 -2
  115. package/src/lib/detect-context.js +2 -2
  116. package/src/lib/execution-approvals.js +59 -0
  117. package/src/lib/knowledge-docker.js +6 -3
  118. package/src/lib/local-sessions.js +148 -0
  119. package/src/lib/local-status.js +1 -1
  120. package/src/lib/project-config.js +1 -1
  121. package/src/lib/provider-credentials.js +183 -0
  122. package/src/lib/scaffold/templates/worker-api/index.js +101 -20
  123. package/src/lib/scaffold/wizards/worker-api.js +27 -11
  124. package/src/lib/slash-commands.js +23 -16
  125. package/src/local/migrations.js +93 -0
  126. package/src/local/runtime-store.js +141 -0
  127. package/src/local/sqlite.js +2 -0
  128. package/src/local-pty/server.js +113 -51
  129. package/src/models/catalog.js +135 -0
  130. package/src/models/discovery.js +292 -0
  131. package/src/models/index.js +7 -0
  132. package/src/providers/anthropic-messages.js +192 -0
  133. package/src/providers/cloudflare-chat.js +183 -0
  134. package/src/providers/factory.js +69 -0
  135. package/src/providers/gemini-generate-content.js +208 -0
  136. package/src/providers/index.js +10 -0
  137. package/src/providers/ollama-chat.js +148 -0
  138. package/src/providers/openai-responses.js +426 -0
  139. package/src/repository/index.js +14 -2
  140. package/src/rpc/generated/common_grpc_pb.js +1 -0
  141. package/src/rpc/generated/common_pb.js +536 -0
  142. package/src/rpc/generated/errors_grpc_pb.js +1 -0
  143. package/src/rpc/generated/errors_pb.js +482 -0
  144. package/src/rpc/generated/knowledge_grpc_pb.js +135 -0
  145. package/src/rpc/generated/knowledge_pb.js +2168 -0
  146. package/src/rpc/generated/package.json +3 -0
  147. package/src/security/process.js +35 -9
  148. package/src/security/trust-boundary.js +2 -2
  149. package/src/telemetry/contracts.js +203 -0
  150. package/src/telemetry/events.js +51 -0
  151. package/src/telemetry/index.js +8 -0
  152. package/src/tools/hydrate.js +35 -0
  153. package/src/tools/index.js +1 -0
  154. package/src/ui/boot.js +15 -17
  155. package/src/ui/cli/activity.js +76 -0
  156. package/src/ui/cli/compaction.js +15 -0
  157. package/src/ui/cli/footer.js +39 -0
  158. package/src/ui/cli/help.js +192 -0
  159. package/src/ui/cli/plan.js +20 -0
  160. package/src/ui/cli/runtime-events.js +110 -0
  161. package/src/ui/cli/waiting.js +16 -0
  162. package/src/ui/merkle/render.js +1 -1
  163. package/test/account-session.test.mjs +36 -0
  164. package/test/cli/preferences-runtime.test.mjs +11 -0
  165. package/test/cli/runtime-ui.test.mjs +74 -0
  166. package/test/cli-preferences.test.mjs +26 -5
  167. package/test/cloudflare-connector.test.mjs +96 -0
  168. package/test/cloudflare-runtime.test.mjs +75 -0
  169. package/test/context.test.mjs +61 -12
  170. package/test/deploy-health-scan.test.mjs +67 -0
  171. package/test/error-diagnostics.test.mjs +115 -0
  172. package/test/eval-context.test.mjs +37 -0
  173. package/test/execution-approvals.test.mjs +27 -0
  174. package/test/fixtures/knowledge-rpc-worker.mjs +16 -0
  175. package/test/integration/cli-help.test.mjs +37 -0
  176. package/test/integration/knowledge-rpc.test.mjs +112 -0
  177. package/test/integration/merkle-cli.test.mjs +61 -0
  178. package/test/integration/merkle-persistence-identity.test.mjs +48 -0
  179. package/test/integration/provider-env-cli.test.mjs +49 -0
  180. package/test/integration/provider-factory.test.mjs +197 -0
  181. package/test/integration/repository-company-graph.test.mjs +90 -0
  182. package/test/integration/runtime-migrations.test.mjs +82 -0
  183. package/test/knowledge-service.test.mjs +5 -0
  184. package/test/knowledge.test.mjs +16 -0
  185. package/test/live/terminal-transport.live.test.mjs +24 -0
  186. package/test/local-sessions.test.mjs +48 -0
  187. package/test/local-studio-deploy.test.mjs +83 -0
  188. package/test/model-catalog.test.mjs +43 -0
  189. package/test/models.test.mjs +127 -16
  190. package/test/npm10-lock.test.mjs +29 -0
  191. package/test/ollama.test.mjs +21 -0
  192. package/test/openai-responses.test.mjs +95 -0
  193. package/test/portable-context.test.mjs +1 -1
  194. package/test/provider-credentials.test.mjs +96 -0
  195. package/test/rehydrate.test.mjs +25 -0
  196. package/test/release-hygiene.test.mjs +13 -5
  197. package/test/responses-runner.test.mjs +150 -0
  198. package/test/shell.test.mjs +92 -23
  199. package/test/smoke.mjs +4 -1
  200. package/test/telemetry.test.mjs +79 -0
  201. package/test/terminal/local-pty.mock.test.mjs +151 -0
  202. package/test/tools-search.test.mjs +14 -1
  203. package/test/whoami-resume.test.mjs +56 -0
  204. /package/{src/lib → packages/agentsam-repository/src}/git-context.js +0 -0
  205. /package/{src/lib → packages/agentsam-repository/src}/merkle/diff.js +0 -0
  206. /package/{src/lib → packages/agentsam-repository/src}/merkle/filemeta.js +0 -0
  207. /package/{src/lib → packages/agentsam-repository/src}/merkle/git-ignore.js +0 -0
  208. /package/{src/lib → packages/agentsam-repository/src}/merkle/hash.js +0 -0
  209. /package/{src/lib → packages/agentsam-repository/src}/merkle/semantic.js +0 -0
  210. /package/{src/lib → packages/agentsam-repository/src}/merkle/snapshot.js +0 -0
  211. /package/{src/lib → packages/agentsam-repository/src}/merkle/tree.js +0 -0
@@ -0,0 +1,150 @@
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, components_usd: { input: total * 0.5, cached_input: total * 0.1, cache_write: total * 0.1, output: total * 0.3 } }; }
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.cost_breakdown_usd.input, 0.25);
77
+ assert.equal(result.cost_breakdown_usd.output, 0.15);
78
+ assert.equal(result.continuation.compact_before_next_turn, false);
79
+ assert.ok(events.some(event => event.type === 'tool.search'));
80
+ assert.ok(events.some(event => event.type === 'tool.completed'));
81
+ });
82
+
83
+ test('runtime approval hook can deny a model-triggered tool before execution', async t => {
84
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-runner-denied-'));
85
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
86
+ fs.writeFileSync(path.join(root, 'package.json'), '{"name":"denied-demo"}');
87
+ let invoked = false;
88
+ const capabilityAdapter = createCapabilityAdapter({
89
+ handlers: {
90
+ 'repository.snapshot': async () => { invoked = true; return { ok: true }; },
91
+ },
92
+ });
93
+ const provider = {
94
+ async create() {
95
+ return {
96
+ response_id: 'resp_deny', output_text: '', actual_service_tier: 'default',
97
+ tool_calls: [{ call_id: 'call_deny', name: capabilityFunctionName('repository.snapshot'), arguments: '{}' }],
98
+ usage_snapshot: usage(10_000), cost: cost(0.01),
99
+ };
100
+ },
101
+ async continueWithToolOutputs() { throw new Error('continuation_should_not_run'); },
102
+ };
103
+ await assert.rejects(() => runResponsesAgent({
104
+ provider, capabilityAdapter, cwd: root, prompt: 'snapshot repository', model: 'gpt-6-astra', reasoningEffort: 'low', serviceTier: 'default',
105
+ beforeTool: async request => {
106
+ assert.equal(request.capability_id, 'repository.snapshot');
107
+ assert.equal(request.cwd, root);
108
+ return false;
109
+ },
110
+ }), /tool_execution_not_approved:repository\.snapshot/);
111
+ assert.equal(invoked, false);
112
+ });
113
+
114
+ test('runner compacts before a projected high-context continuation rather than crossing normal policy blindly', async () => {
115
+ let compactCalls = 0;
116
+ let createInput = null;
117
+ const capabilityAdapter = createCapabilityAdapter();
118
+ const provider = {
119
+ async compact(params) {
120
+ compactCalls += 1;
121
+ assert.equal(params.previousResponseId, 'resp_old');
122
+ return { output: [{ type: 'compaction', encrypted_content: 'opaque-small' }] };
123
+ },
124
+ async create(params) {
125
+ createInput = params.input;
126
+ assert.equal(params.previousResponseId, undefined);
127
+ return { response_id: 'resp_new', output_text: 'ok', actual_service_tier: 'default', tool_calls: [], usage_snapshot: usage(30_000), cost: cost(0.1) };
128
+ },
129
+ async continueWithToolOutputs() { throw new Error('unused'); },
130
+ };
131
+ const result = await runResponsesAgent({
132
+ provider, capabilityAdapter, cwd: process.cwd(), prompt: 'continue repository work', model: 'gpt-6-astra', reasoningEffort: 'low', serviceTier: 'default',
133
+ previousResponseId: 'resp_old', previousUsageSnapshot: usage(179_500),
134
+ });
135
+ assert.equal(compactCalls, 1);
136
+ assert.ok(Array.isArray(createInput));
137
+ assert.equal(createInput[0].type, 'compaction');
138
+ assert.equal(result.compacted_before_turn, true);
139
+ });
140
+
141
+ test('runner refuses an oversized initial context and supports an explicit projected call-cost ceiling', async () => {
142
+ const capabilityAdapter = createCapabilityAdapter();
143
+ const provider = { async create() { throw new Error('provider_should_not_run'); }, async continueWithToolOutputs() {}, async compact() {} };
144
+ await assert.rejects(() => runResponsesAgent({
145
+ provider, capabilityAdapter, cwd: process.cwd(), prompt: 'x'.repeat(800_000), model: 'gpt-6-astra', reasoningEffort: 'low', serviceTier: 'default',
146
+ }), /context_preflight_(pricing_threshold|max_normal|compaction_required)/);
147
+ await assert.rejects(() => runResponsesAgent({
148
+ provider, capabilityAdapter, cwd: process.cwd(), prompt: 'small task', model: 'gpt-6-astra', reasoningEffort: 'low', serviceTier: 'fast', maxCallCostUsd: 0.01,
149
+ }), /projected_call_cost_exceeds_budget/);
150
+ });
@@ -5,6 +5,7 @@ import path from 'node:path';
5
5
  import { spawnSync } from 'node:child_process';
6
6
  import test from 'node:test';
7
7
  import { dispatchShellLine, renderShellCatalog, renderShellPrompt, tokenizeShellLine } from '../src/commands/shell.js';
8
+ import { readCliPreferences, writeCliPreferences } from '../src/lib/cli-preferences.js';
8
9
 
9
10
  const repoRoot = path.resolve(new URL('..', import.meta.url).pathname);
10
11
 
@@ -21,47 +22,115 @@ test('interactive prompt derives username and cwd instead of hardcoding Agent Sa
21
22
  assert.equal(renderShellPrompt('/tmp/demo', env), 'alice /tmp/demo > ');
22
23
  });
23
24
 
24
- test('shell catalog advertises commands that the REPL owns', () => {
25
+ test('shell startup stays quiet and points to the picker', () => {
25
26
  const catalog = renderShellCatalog();
26
- assert.match(catalog, /\/help\s+Show Agent Sam commands/);
27
- assert.match(catalog, /\/status\s+Local project/);
28
- assert.match(catalog, /\/exit\s+Exit Agent Sam shell/);
27
+ for (const command of ['/model', '/usage', '/help', '/exit']) {
28
+ assert.match(catalog, new RegExp(command.replace('/', '\\/')));
29
+ }
30
+ assert.match(catalog, /command picker/);
31
+ assert.match(catalog, /Type normally to work with the selected model/);
32
+ assert.doesNotMatch(catalog, /Slash commands \(/);
33
+ assert.doesNotMatch(catalog, /\/reasoning\s+Set reasoning/);
29
34
  });
30
35
 
31
- test('dispatch handles help, pwd, cd, and exit without falling through to host shell', async () => {
36
+ test('dispatch handles help, menu fallback, pwd, cd, and exit without falling through to host shell', async () => {
32
37
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-shell-'));
33
38
  const child = path.join(root, 'child folder');
34
39
  fs.mkdirSync(child);
35
40
  let output = '';
36
- const state = { cwd: root, write: (text) => { output += text; } };
37
-
41
+ const state = { cwd: root, write: (text) => { output += text; }, interactive: false };
38
42
  let result = await dispatchShellLine('/help', state);
39
43
  assert.equal(result.handled, true);
40
- assert.equal(result.exit, false);
41
- assert.match(output, /Slash commands/);
42
-
44
+ assert.match(output, /Type normally to work with Agent Sam/);
45
+ assert.match(output, /agentsam help <topic>/);
43
46
  output = '';
44
- result = await dispatchShellLine('/pwd', state);
45
- assert.equal(result.handled, true);
47
+ await dispatchShellLine('/', state);
48
+ assert.match(output, /command picker/);
49
+ output = '';
50
+ await dispatchShellLine('/pwd', state);
46
51
  assert.equal(output.trim(), root);
47
-
48
52
  output = '';
49
- result = await dispatchShellLine('/cd "child folder"', state);
50
- assert.equal(result.handled, true);
53
+ await dispatchShellLine('/cd "child folder"', state);
51
54
  assert.equal(state.cwd, child);
52
55
  assert.equal(output.trim(), child);
53
-
54
56
  result = await dispatchShellLine('/exit', state);
55
57
  assert.equal(result.exit, true);
56
58
  });
57
59
 
60
+ test('/usage renders the current session receipt without ending the session', async () => {
61
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-shell-usage-'));
62
+ let output = '';
63
+ const state = {
64
+ cwd: root, interactive: false, write: (text) => { output += text; },
65
+ session: {
66
+ id: 'asess_00000000-0000-4000-8000-000000000001', title: 'Usage test', model_key: 'openai:gpt-6-astra',
67
+ cumulative_usage: { input_tokens: 21_244, cached_input_tokens: 60_544, output_tokens: 219 },
68
+ total_cost_usd: 0.42, cost_breakdown_usd: { input: 0.2, cached_input: 0.02, output: 0.2 },
69
+ },
70
+ };
71
+ const result = await dispatchShellLine('/usage', state);
72
+ assert.equal(result.exit, false);
73
+ assert.match(output, /Token usage: total=21,463 input=21,244 \(\+ 60,544 cached\) output=219/);
74
+ assert.match(output, /Spent: \$0\.4200/);
75
+ assert.match(output, /agentsam resume asess_00000000-0000-4000-8000-000000000001/);
76
+ });
77
+
78
+ test('/logout signs out locally and emits the same resumable usage receipt', async () => {
79
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-shell-logout-'));
80
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-shell-home-'));
81
+ let output = '';
82
+ const state = {
83
+ cwd: root, home, interactive: false, write: (text) => { output += text; },
84
+ session: {
85
+ id: 'asess_00000000-0000-4000-8000-000000000002', title: 'Logout test', model_key: 'openai:gpt-6-astra',
86
+ cumulative_usage: { input_tokens: 100, cached_input_tokens: 50, output_tokens: 25 },
87
+ total_cost_usd: 0.0125, cost_breakdown_usd: { input: 0.005, cached_input: 0.0025, output: 0.005 },
88
+ },
89
+ };
90
+ const result = await dispatchShellLine('/logout', state);
91
+ assert.equal(result.exit, false);
92
+ assert.match(output, /No local Agent Sam IAM session was stored/);
93
+ assert.match(output, /Token usage: total=125 input=100 \(\+ 50 cached\) output=25/);
94
+ assert.match(output, /Spent: \$0\.0125/);
95
+ assert.match(output, /agentsam resume asess_00000000-0000-4000-8000-000000000002/);
96
+ });
97
+
98
+ test('reasoning and service-tier commands persist only supported controls for an exact model', async () => {
99
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-shell-model-'));
100
+ fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'model-demo' }));
101
+ writeCliPreferences(root, { trustedDirectory: true, modelPreference: 'openai:gpt-6-astra', reasoningEffort: 'low', serviceTier: 'default' });
102
+ let output = '';
103
+ const state = { cwd: root, write: (text) => { output += text; }, interactive: false };
104
+ await dispatchShellLine('/reasoning high', state);
105
+ assert.equal(readCliPreferences(root).reasoningEffort, 'high');
106
+ await dispatchShellLine('/fast', state);
107
+ assert.equal(readCliPreferences(root).serviceTier, 'fast');
108
+ await dispatchShellLine('/flex', state);
109
+ assert.equal(readCliPreferences(root).serviceTier, 'flex');
110
+ await dispatchShellLine('/standard', state);
111
+ assert.equal(readCliPreferences(root).serviceTier, 'default');
112
+ assert.match(output, /reasoning → high/);
113
+ assert.match(output, /Fast is a paid latency choice/);
114
+ assert.match(output, /Flex trades latency/);
115
+ });
116
+
117
+ test('bare /context shows truthful economics without inventing active token usage', async () => {
118
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-shell-context-'));
119
+ fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'context-demo' }));
120
+ writeCliPreferences(root, { trustedDirectory: true, modelPreference: 'openai:gpt-6-astra', reasoningEffort: 'medium', serviceTier: 'default' });
121
+ let output = '';
122
+ await dispatchShellLine('/context', { cwd: root, write: (text) => { output += text; }, interactive: false });
123
+ assert.match(output, /1,050,000/);
124
+ assert.match(output, /272,000/);
125
+ assert.match(output, /180,000/);
126
+ assert.match(output, /unavailable · no provider\/local usage snapshot yet/);
127
+ assert.match(output, /Batch is a separate asynchronous execution lane/);
128
+ });
129
+
58
130
  test('CLI supports a deterministic one-shot slash command for regression tests', () => {
59
- const result = spawnSync(process.execPath, ['src/cli.js', 'shell', '--command', '/help'], {
60
- cwd: repoRoot,
61
- encoding: 'utf8',
62
- });
131
+ const result = spawnSync(process.execPath, ['src/cli.js', 'shell', '--command', '/help'], { cwd: repoRoot, encoding: 'utf8' });
63
132
  assert.equal(result.status, 0, result.stderr);
64
- assert.match(result.stdout, /Agent Sam Terminal/);
65
- assert.match(result.stdout, /\/help/);
66
- assert.match(result.stdout, /\/exit/);
133
+ assert.match(result.stdout, /Type normally to work with Agent Sam/);
134
+ assert.match(result.stdout, /agentsam help <topic>/);
135
+ assert.match(result.stdout, /command picker/);
67
136
  });
package/test/smoke.mjs CHANGED
@@ -55,9 +55,12 @@ await assert.rejects(() => finalizeInboundOAuth({}, new Request('https://x'), {}
55
55
  assert.ok(SLASH_COMMANDS.some((c) => c.cmd === '/deploy'));
56
56
  assert.ok(SLASH_COMMANDS.some((c) => c.cmd === '/db'));
57
57
  assert.ok(SLASH_COMMANDS.some((c) => c.cmd === '/agent'));
58
+ assert.ok(SLASH_COMMANDS.some((c) => c.cmd === '/model'));
59
+ assert.ok(SLASH_COMMANDS.some((c) => c.cmd === '/reasoning'));
60
+ assert.ok(SLASH_COMMANDS.some((c) => c.cmd === '/fast'));
58
61
  assert.deepEqual(
59
62
  listSlashCommands({ lane: 'deploy' }).map(({ cmd }) => cmd),
60
- ['/help', '/models', '/settings', '/deploy', '/exit'],
63
+ ['/settings', '/deploy', '/help', '/exit'],
61
64
  'deploy lane keeps global shell controls and excludes commands from other lanes',
62
65
  );
63
66
 
@@ -0,0 +1,79 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { createAgentEvent, createUsageSnapshot } from '../src/telemetry/index.js';
4
+
5
+ test('AgentEvent is compact serializable provider-neutral telemetry', () => {
6
+ const event = createAgentEvent('context.compaction.completed', { before: 181_000, after: 72_000 }, {
7
+ runId: 'run_test', sequence: 4, timestamp: '2026-09-12T00:00:00.000Z',
8
+ });
9
+ assert.equal(event.type, 'context.compaction.completed');
10
+ assert.equal(event.run_id, 'run_test');
11
+ assert.doesNotThrow(() => JSON.stringify(event));
12
+ });
13
+
14
+ test('current active context and cumulative run usage are different metrics', () => {
15
+ const usage = createUsageSnapshot({
16
+ current_context: { input_tokens: 48_000, window_tokens: 1_050_000 },
17
+ cumulative: { input_tokens: 310_000, output_tokens: 22_000, cached_input_tokens: 120_000, cache_write_tokens: 12_000, reasoning_tokens: 8_000 },
18
+ estimate_kind: 'provider',
19
+ });
20
+ assert.equal(usage.current_context.input_tokens, 48_000);
21
+ assert.equal(usage.cumulative.input_tokens, 310_000);
22
+ assert.equal(usage.provider_authoritative, true);
23
+ });
24
+
25
+ import {
26
+ createRunReceipt,
27
+ createUsageReceipt,
28
+ createApprovalReceipt,
29
+ createTerminalJobReceipt,
30
+ } from '../src/telemetry/index.js';
31
+
32
+ test('runtime receipts are account-owned and emit no tenant/workspace/user ownership aliases', () => {
33
+ const run = createRunReceipt({
34
+ id: 'arun_1', account_id: 'au_1', mode: 'agent', status: 'running',
35
+ source_client: 'cli', surface: 'cli', reasoning_effort: 'high', requested_service_tier: 'fast',
36
+ });
37
+ assert.equal(run.account_id, 'au_1');
38
+ assert.equal(run.source_client, 'cli');
39
+ assert.equal(run.requested_service_tier, 'fast');
40
+ assert.equal('user_id' in run, false);
41
+ assert.equal('tenant_id' in run, false);
42
+ assert.equal('workspace_id' in run, false);
43
+ });
44
+
45
+ test('usage receipt models exactly one provider call and keeps cache/reasoning economics', () => {
46
+ const usage = createUsageReceipt({
47
+ id: 'ue_1', account_id: 'au_1', agent_run_id: 'arun_1', provider: 'openai', model_key: 'gpt-6-astra',
48
+ model_call_index: 2, provider_request_id: 'resp_1', input_tokens: 1000, cached_input_tokens: 600,
49
+ cache_write_tokens: 20, output_tokens: 100, reasoning_tokens: 40, cost_usd: 0.12, cost_basis: 'provider',
50
+ });
51
+ assert.equal(usage.total_tokens, 1100);
52
+ assert.equal(usage.cached_input_tokens, 600);
53
+ assert.equal(usage.reasoning_tokens, 40);
54
+ assert.equal(usage.model_call_index, 2);
55
+ });
56
+
57
+ test('approval and terminal receipts preserve lineage without turning D1 into transcript storage', () => {
58
+ const approval = createApprovalReceipt({
59
+ id: 'appr_1', account_id: 'au_1', agent_run_id: 'arun_1', tool_call_id: 'call_1', terminal_job_id: 'tjob_1',
60
+ action_summary: 'Deploy worker', sanitizedInput: { worker: 'api' }, risk_level: 'high',
61
+ });
62
+ assert.equal(approval.terminal_job_id, 'tjob_1');
63
+ assert.match(approval.sanitized_input_json, /worker/);
64
+
65
+ const job = createTerminalJobReceipt({
66
+ id: 'tjob_1', account_id: 'au_1', instance_id: 'tinst_1', connection_id: 'conn_1',
67
+ execos_run_id: 'xrun_1', status: 'running', idempotency_key: 'idem_1', max_attempts: 1,
68
+ });
69
+ assert.equal(job.execos_run_id, 'xrun_1');
70
+ assert.equal(job.max_attempts, 1);
71
+ assert.equal('stdout' in job, false);
72
+ assert.equal('stderr' in job, false);
73
+ assert.equal('command' in job, false);
74
+ });
75
+
76
+ test('runtime receipts refuse model-invented account identity', () => {
77
+ assert.throws(() => createRunReceipt({ id: 'arun_1' }), /account_id is required/);
78
+ assert.throws(() => createUsageReceipt({ id: 'ue_1', provider: 'openai', model_key: 'gpt-6-astra' }), /account_id is required/);
79
+ });
@@ -0,0 +1,151 @@
1
+ import assert from 'node:assert/strict';
2
+ import { EventEmitter } from 'node:events';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import test from 'node:test';
7
+ import { pathToFileURL } from 'node:url';
8
+ import {
9
+ attachLocalPtySession,
10
+ ensureNodePtySpawnHelperExecutable,
11
+ startLocalPtyServer,
12
+ } from '../../src/local-pty/server.js';
13
+
14
+ function fakeTransports() {
15
+ const writes = [];
16
+ const resizes = [];
17
+ let killed = 0;
18
+ let dataHandler = null;
19
+ let exitHandler = null;
20
+
21
+ const pty = {
22
+ spawn(shell, args, options) {
23
+ assert.equal(Array.isArray(args), true);
24
+ assert.equal(options.env.AGENTSAM_LOCAL_PTY, '1');
25
+ return {
26
+ write(value) { writes.push(value); },
27
+ resize(cols, rows) { resizes.push([cols, rows]); },
28
+ kill() { killed += 1; },
29
+ onData(fn) { dataHandler = fn; },
30
+ onExit(fn) { exitHandler = fn; },
31
+ };
32
+ },
33
+ };
34
+
35
+ class FakeWs extends EventEmitter {
36
+ OPEN = 1;
37
+ readyState = 1;
38
+ sent = [];
39
+ send(value) { this.sent.push(String(value)); }
40
+ close() {
41
+ if (this.readyState !== this.OPEN) return;
42
+ this.readyState = 3;
43
+ this.emit('close');
44
+ }
45
+ }
46
+
47
+ const ws = new FakeWs();
48
+ return {
49
+ pty,
50
+ ws,
51
+ writes,
52
+ resizes,
53
+ killed: () => killed,
54
+ emitData(value) { dataHandler?.(value); },
55
+ emitExit() { exitHandler?.(); },
56
+ };
57
+ }
58
+
59
+ test('macOS node-pty helper is made executable before runtime import', (t) => {
60
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-node-pty-helper-'));
61
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
62
+ const libDir = path.join(root, 'lib');
63
+ const helperDir = path.join(root, 'prebuilds', 'darwin-arm64');
64
+ fs.mkdirSync(libDir, { recursive: true });
65
+ fs.mkdirSync(helperDir, { recursive: true });
66
+ fs.writeFileSync(path.join(libDir, 'index.js'), '');
67
+ const helper = path.join(helperDir, 'spawn-helper');
68
+ fs.writeFileSync(helper, 'fake');
69
+ fs.chmodSync(helper, 0o644);
70
+
71
+ const result = ensureNodePtySpawnHelperExecutable({
72
+ platform: 'darwin',
73
+ arch: 'arm64',
74
+ resolveModule: () => pathToFileURL(path.join(libDir, 'index.js')).href,
75
+ });
76
+
77
+ assert.equal(result.checked, true);
78
+ assert.equal(result.changed, true);
79
+ assert.equal(result.path, helper);
80
+ assert.notEqual(fs.statSync(helper).mode & 0o111, 0);
81
+
82
+ const second = ensureNodePtySpawnHelperExecutable({
83
+ platform: 'darwin',
84
+ arch: 'arm64',
85
+ resolveModule: () => pathToFileURL(path.join(libDir, 'index.js')).href,
86
+ });
87
+ assert.equal(second.changed, false);
88
+ });
89
+
90
+ test('local PTY wire protocol is release-testable entirely in memory', () => {
91
+ const transport = fakeTransports();
92
+ const session = attachLocalPtySession({
93
+ ws: transport.ws,
94
+ pty: transport.pty,
95
+ shell: '/bin/test-shell',
96
+ cwd: '/tmp/test-project',
97
+ cols: 80,
98
+ rows: 24,
99
+ env: {},
100
+ sessionId: 'local_test',
101
+ });
102
+
103
+ assert.equal(session.session_id, 'local_test');
104
+ assert.deepEqual(JSON.parse(transport.ws.sent[0]), {
105
+ type: 'session_id',
106
+ session_id: 'local_test',
107
+ });
108
+
109
+ transport.ws.emit('message', Buffer.from(JSON.stringify({ type: 'resize', cols: 120, rows: 40 })));
110
+ transport.ws.emit('message', Buffer.from(JSON.stringify({ type: 'slash', line: '/pwd' })));
111
+ transport.ws.emit('message', Buffer.from('raw input'));
112
+
113
+ assert.deepEqual(transport.resizes, [[120, 40]]);
114
+ assert.deepEqual(transport.writes, ['/pwd\r', 'raw input']);
115
+
116
+ transport.emitData('terminal output');
117
+ assert.equal(transport.ws.sent.at(-1), 'terminal output');
118
+
119
+ transport.ws.close();
120
+ assert.equal(transport.killed(), 1);
121
+
122
+ // Cleanup is idempotent even when transport error/close paths race.
123
+ transport.ws.emit('error', new Error('synthetic transport close'));
124
+ session.cleanup();
125
+ assert.equal(transport.killed(), 1);
126
+ });
127
+
128
+ test('local PTY server health check uses an ephemeral local port and no ExecOS tunnel', async () => {
129
+ const transport = fakeTransports();
130
+ const server = await startLocalPtyServer({
131
+ cwd: process.cwd(),
132
+ host: '127.0.0.1',
133
+ port: 0,
134
+ pty: transport.pty,
135
+ });
136
+
137
+ try {
138
+ const health = await fetch(server.healthUrl);
139
+ assert.equal(health.status, 200);
140
+ const body = await health.json();
141
+ assert.equal(body.ok, true);
142
+ assert.equal(body.service, 'agentsam-local-pty');
143
+ assert.equal(body.port, server.port);
144
+ assert.equal(server.port > 0, true);
145
+ } finally {
146
+ await server.close();
147
+ }
148
+
149
+ // No WebSocket session was opened, so the PTY process was never spawned.
150
+ assert.equal(transport.killed(), 0);
151
+ });
@@ -1,7 +1,7 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import test from 'node:test';
3
3
  import { getToolCatalog } from '../src/lib/tools.js';
4
- import { searchToolCards } from '../src/tools/index.js';
4
+ import { hydrateToolSchemas, searchToolCards } from '../src/tools/index.js';
5
5
  import { DEFAULT_RESULT_POLICY } from '../src/context/index.js';
6
6
 
7
7
  test('every built-in SDK tool inherits a bounded result policy', () => {
@@ -25,3 +25,16 @@ test('tools.search returns compact cards rather than full schemas', () => {
25
25
  assert.equal('input_schema' in result.cards[0], false);
26
26
  assert.ok(result.receipt.chars < 24_000);
27
27
  });
28
+
29
+ test('selected schemas hydrate without loading the full tool catalog', () => {
30
+ const catalog = Array.from({ length: 50 }, (_, index) => ({
31
+ name: `tool.${index}`,
32
+ description: `Capability ${index}`,
33
+ input_schema: { type: 'object', properties: { value: { type: 'string' }, padding: { description: 'x'.repeat(300) } } },
34
+ }));
35
+ const result = hydrateToolSchemas(catalog, ['tool.4', 'tool.22'], { maxTools: 4, maxChars: 5_000 });
36
+ assert.deepEqual(result.tools.map((tool) => tool.name), ['tool.4', 'tool.22']);
37
+ assert.equal(result.receipt.catalog_items, 50);
38
+ assert.equal(result.receipt.hydrated_tools, 2);
39
+ assert.ok(result.receipt.schema_chars < 5_000);
40
+ });
@@ -0,0 +1,56 @@
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 { saveAccountSession } from '../src/lib/account-session.js';
7
+ import { createLocalSession } from '../src/lib/local-sessions.js';
8
+ import { collectWhoami } from '../src/commands/whoami.js';
9
+ import { runResume } from '../src/commands/resume.js';
10
+
11
+ function tempHome(t) {
12
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-ux-home-'));
13
+ t.after(() => fs.rmSync(home, { recursive: true, force: true }));
14
+ return home;
15
+ }
16
+
17
+ test('whoami validates persisted IAM identity while never returning the SDK or provider secret', async t => {
18
+ const home = tempHome(t);
19
+ saveAccountSession({ access_token: 'sdk_do_not_print', user_id: 'au_local' }, { home });
20
+ const envDir = path.join(home, '.agentsam', 'env.d');
21
+ fs.mkdirSync(envDir, { recursive: true });
22
+ const openaiFile = path.join(envDir, 'openai.env');
23
+ fs.writeFileSync(openaiFile, 'OPENAI_API_KEY=sk-never-print-this\n', { mode: 0o600 });
24
+ if (process.platform !== 'win32') fs.chmodSync(openaiFile, 0o600);
25
+
26
+ const status = await collectWhoami({
27
+ env: {}, home,
28
+ contextLoader: async token => {
29
+ assert.equal(token, 'sdk_do_not_print');
30
+ return { user_id: 'au_server', account_id: 'acct_server', email: 'dev@example.test', cloudflare: { ok: true }, byok: { openai: { configured: true, masked: 'secret' } } };
31
+ },
32
+ });
33
+ assert.equal(status.authenticated, true);
34
+ assert.equal(status.identity.account_id, 'acct_server');
35
+ assert.equal(status.sdk_credential.source, 'agentsam_account_session');
36
+ assert.equal(status.provider_credentials.find(row => row.provider === 'openai').configured, true);
37
+ const serialized = JSON.stringify(status);
38
+ assert.doesNotMatch(serialized, /sdk_do_not_print|sk-never-print-this|masked/);
39
+ });
40
+
41
+ test('resume restores saved cwd and session through the canonical shell runtime', async t => {
42
+ const home = tempHome(t);
43
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-resume-project-'));
44
+ t.after(() => fs.rmSync(cwd, { recursive: true, force: true }));
45
+ const session = createLocalSession({ cwd, title: 'Run wrangler whoami', model_key: 'openai:gpt-6-astra' }, { home });
46
+ let called = null;
47
+ const restored = await runResume([session.id], {
48
+ home,
49
+ write() {},
50
+ runShellImpl: async (argv, options) => { called = { argv, options }; },
51
+ });
52
+ assert.equal(restored.id, session.id);
53
+ assert.deepEqual(called.argv, []);
54
+ assert.equal(called.options.cwd, cwd);
55
+ assert.equal(called.options.session.id, session.id);
56
+ });