@inneranimalmedia/agentsam-sdk 2.6.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.
- package/docs/PLATFORM_RUNTIME_EVENTS.md +48 -0
- package/docs/RELEASES.md +7 -7
- package/docs/SOURCE_ARCHITECTURE.md +58 -0
- package/docs/TEST_TIERS.md +26 -0
- package/migrations/runtime/0001_cli_runtime.sql +298 -0
- package/package.json +28 -7
- package/packages/agentsam-repository/README.md +15 -0
- package/packages/agentsam-repository/package.json +25 -0
- package/packages/agentsam-repository/src/contracts.js +113 -0
- package/packages/agentsam-repository/src/index.js +3 -0
- package/{src/lib → packages/agentsam-repository/src}/merkle/cloudflare-persistence.js +14 -24
- package/{src/lib → packages/agentsam-repository/src}/merkle/index.js +1 -0
- package/{src/lib → packages/agentsam-repository/src}/merkle/persistence.js +6 -4
- package/{src/lib → packages/agentsam-repository/src}/merkle/policy.js +1 -0
- package/packages/agentsam-repository/test/contracts.test.mjs +40 -0
- package/packages/agentsam-repository/test/git-context.test.mjs +24 -0
- package/{test/merkle.test.mjs → packages/agentsam-repository/test/merkle-core.test.mjs} +2 -32
- package/{test → packages/agentsam-repository/test}/merkle-persistence.test.mjs +11 -6
- package/packages/identity/package.json +1 -1
- package/protocol/COMPANY_REPOSITORY_GRAPH_V1.md +91 -0
- package/protocol/MERKLE_PERSISTENCE_V1.md +2 -0
- package/protocol/MERKLE_PERSISTENCE_V2.md +40 -0
- package/protocol/repository/repository-contract.schema.json +24 -0
- package/protocol/repository/repository-dependency.schema.json +24 -0
- package/protocol/repository/repository-identity.schema.json +17 -0
- package/protocol/rpc/v1/common.proto +16 -0
- package/protocol/rpc/v1/errors.proto +35 -0
- package/protocol/rpc/v1/knowledge.proto +77 -0
- package/services/knowledge/package-lock.json +333 -0
- package/services/knowledge/package.json +5 -1
- package/src/agent/responses-runner.js +63 -35
- package/src/capabilities/repository-snapshot.js +3 -3
- package/src/cli.js +23 -6
- package/src/commands/context-economics.js +17 -2
- package/src/commands/context.js +1 -1
- package/src/commands/db.js +20 -3
- package/src/commands/env.js +90 -0
- package/src/commands/knowledge.js +12 -4
- package/src/commands/merkle-persist.js +30 -11
- package/src/commands/merkle.js +1 -1
- package/src/commands/models.js +123 -65
- package/src/commands/ollama.js +26 -0
- package/src/commands/preferences.js +53 -26
- package/src/commands/shell.js +236 -48
- package/src/errors/contract.js +236 -0
- package/src/errors/index.js +14 -0
- package/src/index.js +13 -1
- package/src/knowledge/service/auth.js +13 -0
- package/src/knowledge/service/grpc-client.js +115 -0
- package/src/knowledge/service/grpc-codec.js +237 -0
- package/src/knowledge/service/grpc-server.js +83 -0
- package/src/knowledge/service/job-engine.js +248 -0
- package/src/knowledge/service/server.js +87 -135
- package/src/knowledge/source.js +1 -1
- package/src/lib/cli-preferences.js +31 -4
- package/src/lib/deploy-receipt/index.js +2 -2
- package/src/lib/knowledge-docker.js +6 -3
- package/src/lib/local-sessions.js +23 -2
- package/src/lib/local-status.js +1 -1
- package/src/lib/project-config.js +1 -1
- package/src/lib/provider-credentials.js +105 -5
- package/src/lib/slash-commands.js +4 -3
- package/src/local/migrations.js +93 -0
- package/src/local/runtime-store.js +141 -0
- package/src/local/sqlite.js +2 -0
- package/src/local-pty/server.js +113 -51
- package/src/models/discovery.js +292 -0
- package/src/providers/anthropic-messages.js +192 -0
- package/src/providers/cloudflare-chat.js +183 -0
- package/src/providers/factory.js +69 -0
- package/src/providers/gemini-generate-content.js +208 -0
- package/src/providers/index.js +5 -0
- package/src/providers/ollama-chat.js +148 -0
- package/src/providers/openai-responses.js +226 -75
- package/src/repository/index.js +14 -2
- package/src/rpc/generated/common_grpc_pb.js +1 -0
- package/src/rpc/generated/common_pb.js +536 -0
- package/src/rpc/generated/errors_grpc_pb.js +1 -0
- package/src/rpc/generated/errors_pb.js +482 -0
- package/src/rpc/generated/knowledge_grpc_pb.js +135 -0
- package/src/rpc/generated/knowledge_pb.js +2168 -0
- package/src/rpc/generated/package.json +3 -0
- package/src/security/trust-boundary.js +2 -2
- package/src/telemetry/events.js +4 -1
- package/src/ui/cli/activity.js +76 -0
- package/src/ui/cli/compaction.js +15 -0
- package/src/ui/cli/footer.js +39 -0
- package/src/ui/cli/help.js +192 -0
- package/src/ui/cli/plan.js +20 -0
- package/src/ui/cli/runtime-events.js +110 -0
- package/src/ui/cli/waiting.js +16 -0
- package/src/ui/merkle/render.js +1 -1
- package/test/cli/preferences-runtime.test.mjs +11 -0
- package/test/cli/runtime-ui.test.mjs +74 -0
- package/test/error-diagnostics.test.mjs +57 -1
- package/test/fixtures/knowledge-rpc-worker.mjs +16 -0
- package/test/integration/cli-help.test.mjs +37 -0
- package/test/integration/knowledge-rpc.test.mjs +112 -0
- package/test/integration/merkle-cli.test.mjs +61 -0
- package/test/integration/merkle-persistence-identity.test.mjs +48 -0
- package/test/integration/provider-env-cli.test.mjs +49 -0
- package/test/integration/provider-factory.test.mjs +197 -0
- package/test/integration/repository-company-graph.test.mjs +90 -0
- package/test/integration/runtime-migrations.test.mjs +82 -0
- package/test/knowledge-service.test.mjs +5 -0
- package/test/knowledge.test.mjs +16 -0
- package/test/live/terminal-transport.live.test.mjs +24 -0
- package/test/local-sessions.test.mjs +7 -1
- package/test/models.test.mjs +101 -4
- package/test/ollama.test.mjs +21 -0
- package/test/portable-context.test.mjs +1 -1
- package/test/provider-credentials.test.mjs +45 -1
- package/test/release-hygiene.test.mjs +13 -5
- package/test/responses-runner.test.mjs +3 -1
- package/test/shell.test.mjs +50 -8
- package/test/terminal/local-pty.mock.test.mjs +151 -0
- /package/{src/lib → packages/agentsam-repository/src}/git-context.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/diff.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/filemeta.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/git-ignore.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/hash.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/semantic.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/snapshot.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/tree.js +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
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 { createLocalSqliteDatabase } from '../../src/local/sqlite.js';
|
|
7
|
+
import { applyRuntimeMigrations } from '../../src/local/migrations.js';
|
|
8
|
+
|
|
9
|
+
test('portable runtime migration installs AgentSam CLI state tables idempotently', async (t) => {
|
|
10
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-runtime-migration-'));
|
|
11
|
+
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
12
|
+
const db = await createLocalSqliteDatabase(path.join(root, 'agentsam.sqlite'));
|
|
13
|
+
try {
|
|
14
|
+
const first = await applyRuntimeMigrations(db);
|
|
15
|
+
assert.equal(first.applied, 1);
|
|
16
|
+
|
|
17
|
+
const tables = await db.prepare(
|
|
18
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'agentsam_%' ORDER BY name"
|
|
19
|
+
).all();
|
|
20
|
+
const names = new Set(tables.results.map((row) => row.name));
|
|
21
|
+
for (const name of [
|
|
22
|
+
'agentsam_agent_run',
|
|
23
|
+
'agentsam_approval_queue',
|
|
24
|
+
'agentsam_compaction_events',
|
|
25
|
+
'agentsam_context_digest',
|
|
26
|
+
'agentsam_cron_runs',
|
|
27
|
+
'agentsam_plans',
|
|
28
|
+
'agentsam_schema_migrations',
|
|
29
|
+
'agentsam_todo',
|
|
30
|
+
]) assert.equal(names.has(name), true, name);
|
|
31
|
+
|
|
32
|
+
const timers = await db.prepare(
|
|
33
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='active_timers'"
|
|
34
|
+
).first();
|
|
35
|
+
assert.equal(timers.name, 'active_timers');
|
|
36
|
+
|
|
37
|
+
const second = await applyRuntimeMigrations(db);
|
|
38
|
+
assert.equal(second.applied, 0);
|
|
39
|
+
assert.equal(second.results[0].status, 'already_applied');
|
|
40
|
+
} finally {
|
|
41
|
+
db.close();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('ephemeral context digests expire and refresh while durable digests stay durable', async (t) => {
|
|
46
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-digest-migration-'));
|
|
47
|
+
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
48
|
+
const db = await createLocalSqliteDatabase(path.join(root, 'agentsam.sqlite'));
|
|
49
|
+
try {
|
|
50
|
+
await applyRuntimeMigrations(db);
|
|
51
|
+
|
|
52
|
+
await db.prepare(
|
|
53
|
+
"INSERT INTO agentsam_context_digest (id,digest_type,source_hash,digest_hash,digest_text) VALUES (?,?,?,?,?)"
|
|
54
|
+
).bind('repo_1', 'repo', 's1', 'h1', 'durable').run();
|
|
55
|
+
const durable = await db.prepare(
|
|
56
|
+
'SELECT expires_at_unix FROM agentsam_context_digest WHERE id=?'
|
|
57
|
+
).bind('repo_1').first();
|
|
58
|
+
assert.equal(durable.expires_at_unix, null);
|
|
59
|
+
|
|
60
|
+
await db.prepare(
|
|
61
|
+
"INSERT INTO agentsam_context_digest (id,digest_type,source_hash,digest_hash,digest_text) VALUES (?,?,?,?,?)"
|
|
62
|
+
).bind('session_1', 'session', 's2', 'h2', 'ephemeral').run();
|
|
63
|
+
const initial = await db.prepare(
|
|
64
|
+
'SELECT expires_at_unix FROM agentsam_context_digest WHERE id=?'
|
|
65
|
+
).bind('session_1').first();
|
|
66
|
+
assert.equal(Number(initial.expires_at_unix) > 0, true);
|
|
67
|
+
|
|
68
|
+
await db.prepare(
|
|
69
|
+
'UPDATE agentsam_context_digest SET expires_at_unix = unixepoch() + 60 WHERE id=?'
|
|
70
|
+
).bind('session_1').run();
|
|
71
|
+
await db.prepare(
|
|
72
|
+
'UPDATE agentsam_context_digest SET hit_count = hit_count + 1 WHERE id=?'
|
|
73
|
+
).bind('session_1').run();
|
|
74
|
+
const refreshed = await db.prepare(
|
|
75
|
+
'SELECT expires_at_unix, hit_count FROM agentsam_context_digest WHERE id=?'
|
|
76
|
+
).bind('session_1').first();
|
|
77
|
+
assert.equal(refreshed.hit_count, 1);
|
|
78
|
+
assert.equal(Number(refreshed.expires_at_unix) > Math.floor(Date.now() / 1000) + 2_500_000, true);
|
|
79
|
+
} finally {
|
|
80
|
+
db.close();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
@@ -88,6 +88,11 @@ test('portable dockerize stages only SDK runtime, preserves identities/tokens, a
|
|
|
88
88
|
const context = path.join(result.written.versionDir, 'context');
|
|
89
89
|
assert.equal(fs.existsSync(path.join(context, 'customer-secret.js')), false);
|
|
90
90
|
assert.equal(fs.existsSync(path.join(context, 'src/knowledge/service/server.js')), true);
|
|
91
|
+
assert.equal(fs.existsSync(path.join(context, 'src/knowledge/service/grpc-server.js')), true);
|
|
92
|
+
assert.equal(fs.existsSync(path.join(context, 'src/rpc/generated/knowledge_pb.js')), true);
|
|
93
|
+
assert.equal(fs.existsSync(path.join(context, 'src/rpc/generated/knowledge_grpc_pb.js')), true);
|
|
94
|
+
assert.equal(fs.existsSync(path.join(context, 'src/rpc/generated/package.json')), true);
|
|
95
|
+
assert.equal(fs.existsSync(path.join(context, 'src/errors/contract.js')), true);
|
|
91
96
|
assert.equal(fs.existsSync(path.join(context, 'package-lock.json')), true);
|
|
92
97
|
const token = fs.readFileSync(result.tokenFile, 'utf8');
|
|
93
98
|
assert.equal(fs.statSync(result.tokenFile).mode & 0o777, 0o600);
|
package/test/knowledge.test.mjs
CHANGED
|
@@ -114,6 +114,22 @@ test('source safety and setup preserve repository files; read-only plan creates
|
|
|
114
114
|
assert.ok(!paths.includes('.env.js')); assert.ok(!paths.includes('ignored/hidden.ts'));
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
+
test('index and search derive safe local defaults when knowledge.json is absent', async t => {
|
|
118
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-configless-'));
|
|
119
|
+
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
120
|
+
execFileSync('git', ['init', '-q'], { cwd: root });
|
|
121
|
+
execFileSync('git', ['remote', 'add', 'origin', 'git@github.com:ExampleOrg/ConfiglessRepo.git'], { cwd: root });
|
|
122
|
+
write(root, 'src/real.ts', 'export function configlessSymbol() { return 7; }\n');
|
|
123
|
+
const run = args => JSON.parse(execFileSync(process.execPath, [cli, ...args], { cwd: root, encoding: 'utf8' }));
|
|
124
|
+
assert.equal(fs.existsSync(path.join(root, '.agentsam/knowledge.json')), false);
|
|
125
|
+
const plan = run(['index', 'plan']);
|
|
126
|
+
assert.ok(plan.files >= 1); assert.equal(plan.embedding_inputs, 0);
|
|
127
|
+
assert.equal(fs.existsSync(path.join(root, '.agentsam/knowledge.json')), false);
|
|
128
|
+
assert.equal(run(['index', 'run']).published, true);
|
|
129
|
+
assert.equal(fs.existsSync(path.join(root, '.agentsam/knowledge.json')), false);
|
|
130
|
+
assert.equal(run(['search', 'configlessSymbol']).hits[0].path, 'src/real.ts');
|
|
131
|
+
});
|
|
132
|
+
|
|
117
133
|
test('two independent repositories work through the same CLI and exported package', async t => {
|
|
118
134
|
for (const name of ['customer-a', 'customer-b']) {
|
|
119
135
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), name));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { WebSocket } from 'ws';
|
|
4
|
+
|
|
5
|
+
const liveUrl = String(process.env.AGENTSAM_LIVE_TERMINAL_WS_URL || '').trim();
|
|
6
|
+
|
|
7
|
+
test('live terminal transport accepts a websocket connection', { skip: !liveUrl }, async (t) => {
|
|
8
|
+
const ws = new WebSocket(liveUrl, {
|
|
9
|
+
headers: process.env.AGENTSAM_LIVE_TERMINAL_AUTH
|
|
10
|
+
? { authorization: process.env.AGENTSAM_LIVE_TERMINAL_AUTH }
|
|
11
|
+
: undefined,
|
|
12
|
+
});
|
|
13
|
+
t.after(() => { try { ws.close(); } catch {} });
|
|
14
|
+
|
|
15
|
+
await Promise.race([
|
|
16
|
+
new Promise((resolve, reject) => {
|
|
17
|
+
ws.once('open', resolve);
|
|
18
|
+
ws.once('error', reject);
|
|
19
|
+
}),
|
|
20
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('live terminal websocket open timed out')), 8000)),
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
assert.equal(ws.readyState, WebSocket.OPEN);
|
|
24
|
+
});
|
|
@@ -27,6 +27,10 @@ test('local sessions persist provider-neutral continuation, usage, cost, and las
|
|
|
27
27
|
usage_snapshot: { current_context: { input_tokens: 21_244, window_tokens: 1_050_000 } },
|
|
28
28
|
cumulative_usage: { input_tokens: 21_244, cached_input_tokens: 60_544, output_tokens: 219, reasoning_tokens: 31 },
|
|
29
29
|
total_cost_usd: 0.123456,
|
|
30
|
+
cost_breakdown_usd: { input: 0.08, cached_input: 0.01, cache_write: 0.003456, output: 0.03 },
|
|
31
|
+
status: 'paused',
|
|
32
|
+
active_elapsed_ms: 90_000,
|
|
33
|
+
active_started_at: null,
|
|
30
34
|
}, { home });
|
|
31
35
|
|
|
32
36
|
const loaded = loadLocalSession(session.id, { home });
|
|
@@ -36,7 +40,9 @@ test('local sessions persist provider-neutral continuation, usage, cost, and las
|
|
|
36
40
|
|
|
37
41
|
const receipt = renderSessionReceipt(loaded);
|
|
38
42
|
assert.match(receipt, /Token usage: total=21,463 input=21,244 \(\+ 60,544 cached\) output=219 reasoning=31/);
|
|
39
|
-
assert.match(receipt, /
|
|
43
|
+
assert.match(receipt, /Spent: \$0\.1235/);
|
|
44
|
+
assert.match(receipt, /Elapsed: 1m 30s/);
|
|
45
|
+
assert.match(receipt, /Cost breakdown: input \$0\.0800 · cached \$0\.0100 · cache write \$0\.003456 · output \$0\.0300/);
|
|
40
46
|
assert.match(receipt, new RegExp(`agentsam resume ${session.id}`));
|
|
41
47
|
assert.match(receipt, /Run wrangler whoami/);
|
|
42
48
|
});
|
package/test/models.test.mjs
CHANGED
|
@@ -21,7 +21,7 @@ test('model inventory reports configured API providers without exposing credenti
|
|
|
21
21
|
home,
|
|
22
22
|
discoverRemote: false,
|
|
23
23
|
env: {
|
|
24
|
-
OPENAI_API_KEY: 'secret-openai', GEMINI_API_KEY: '', XAI_API_KEY: 'secret-xai', ANTHROPIC_API_KEY: 'secret-anthropic',
|
|
24
|
+
OPENAI_API_KEY: 'secret-openai', GEMINI_API_KEY: '', XAI_API_KEY: 'secret-xai', ANTHROPIC_API_KEY: 'secret-anthropic', CLOUDFLARE_API_TOKEN: 'secret-cf', ACCOUNT_ID: '33333333333333333333333333333333',
|
|
25
25
|
OLLAMA_BASE_URL: 'http://127.0.0.1:11434', OLLAMA_MODEL: 'qwen:test', OLLAMA_EMBED_MODEL: 'embed:test',
|
|
26
26
|
},
|
|
27
27
|
fetchImpl: async () => response({ models: [{ name: 'qwen:test' }, { name: 'embed:test' }] }),
|
|
@@ -30,12 +30,26 @@ test('model inventory reports configured API providers without exposing credenti
|
|
|
30
30
|
assert.equal(status.providers.find((row) => row.id === 'gemini').configured, false);
|
|
31
31
|
assert.equal(status.providers.find((row) => row.id === 'grok').configured, true);
|
|
32
32
|
assert.equal(status.providers.find((row) => row.id === 'anthropic').configured, true);
|
|
33
|
+
assert.equal(status.providers.find((row) => row.id === 'cloudflare').configured, true);
|
|
33
34
|
assert.equal(status.local.online, true);
|
|
34
35
|
assert.deepEqual(status.local.models.map((row) => row.name), ['qwen:test', 'embed:test']);
|
|
35
36
|
const rendered = renderModelsStatus(status);
|
|
36
37
|
assert.match(rendered, /OpenAI/);
|
|
37
38
|
assert.match(rendered, /qwen:test/);
|
|
38
|
-
assert.doesNotMatch(rendered, /secret-openai|secret-xai|secret-anthropic/);
|
|
39
|
+
assert.doesNotMatch(rendered, /secret-openai|secret-xai|secret-anthropic|secret-cf/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('static/reference metadata never invents hosted model availability', async () => {
|
|
43
|
+
const status = await collectModelsStatus({
|
|
44
|
+
env: { OPENAI_API_KEY: 'secret-openai', OLLAMA_BASE_URL: 'http://127.0.0.1:11434' },
|
|
45
|
+
fetchImpl: async () => response({ models: [] }),
|
|
46
|
+
providerFetchImpl: async () => response({ data: [{ id: 'some-other-model' }] }),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
assert.deepEqual(status.availableModels.map((row) => row.provider_model_id), ['some-other-model']);
|
|
50
|
+
const referenceOnly = status.catalogModels.find((row) => row.provider_model_id === 'gpt-6-astra');
|
|
51
|
+
assert.equal(referenceOnly?.availability, 'unverified');
|
|
52
|
+
assert.equal(referenceOnly?.availability_source, 'sdk_reference');
|
|
39
53
|
});
|
|
40
54
|
|
|
41
55
|
test('exact hosted model availability is verified against the provider inventory', async () => {
|
|
@@ -46,6 +60,89 @@ test('exact hosted model availability is verified against the provider inventory
|
|
|
46
60
|
});
|
|
47
61
|
assert.equal(status.discovery.openai.ok, true);
|
|
48
62
|
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.
|
|
63
|
+
assert.deepEqual(status.availableModels.map((row) => row.provider_model_id), ['gpt-6-astra', 'some-other-model']);
|
|
64
|
+
assert.equal(status.availableModels[0].availability_source, 'provider_api');
|
|
65
|
+
assert.equal(status.availableModels[0].context_window_source, 'sdk_reference');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
test('Gemini and xAI discovery keep per-key limits from provider metadata', async () => {
|
|
72
|
+
const seen = [];
|
|
73
|
+
const status = await collectModelsStatus({
|
|
74
|
+
env: {
|
|
75
|
+
GEMINI_API_KEY: 'gem-key',
|
|
76
|
+
XAI_API_KEY: 'xai-key',
|
|
77
|
+
OLLAMA_BASE_URL: 'http://127.0.0.1:11434',
|
|
78
|
+
},
|
|
79
|
+
fetchImpl: async () => response({ models: [] }),
|
|
80
|
+
providerFetchImpl: async (url, options) => {
|
|
81
|
+
seen.push(url);
|
|
82
|
+
if (url.includes('generativelanguage.googleapis.com')) {
|
|
83
|
+
return response({ models: [{
|
|
84
|
+
name: 'models/gemini-test',
|
|
85
|
+
baseModelId: 'gemini-test',
|
|
86
|
+
displayName: 'Gemini Test',
|
|
87
|
+
inputTokenLimit: 123456,
|
|
88
|
+
outputTokenLimit: 8192,
|
|
89
|
+
supportedGenerationMethods: ['generateContent'],
|
|
90
|
+
thinking: true,
|
|
91
|
+
}] });
|
|
92
|
+
}
|
|
93
|
+
if (url.includes('api.x.ai')) {
|
|
94
|
+
return response({ data: [{
|
|
95
|
+
id: 'grok-test',
|
|
96
|
+
context_length: 256000,
|
|
97
|
+
prompt_text_token_price: 1000,
|
|
98
|
+
cached_prompt_text_token_price: 500,
|
|
99
|
+
completion_text_token_price: 4000,
|
|
100
|
+
}] });
|
|
101
|
+
}
|
|
102
|
+
throw new Error('unexpected URL ' + url);
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const gemini = status.providerModels.gemini[0];
|
|
107
|
+
assert.equal(gemini.provider_model_id, 'gemini-test');
|
|
108
|
+
assert.equal(gemini.context_window, 123456);
|
|
109
|
+
assert.equal(gemini.context_window_source, 'provider_api');
|
|
110
|
+
assert.deepEqual(gemini.reasoning_efforts, ['auto', 'low', 'medium', 'high']);
|
|
111
|
+
|
|
112
|
+
const grok = status.providerModels.grok[0];
|
|
113
|
+
assert.equal(grok.provider_model_id, 'grok-test');
|
|
114
|
+
assert.equal(grok.context_window, 256000);
|
|
115
|
+
assert.equal(grok.pricing.input, 1);
|
|
116
|
+
assert.equal(grok.pricing.output, 4);
|
|
117
|
+
assert.equal(seen.some((url) => url.includes('generativelanguage.googleapis.com')), true);
|
|
118
|
+
assert.equal(seen.some((url) => url.includes('api.x.ai')), true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
test('Cloudflare discovery is scoped to the loaded account and surfaces text-generation models only', async t => {
|
|
123
|
+
const seen = [];
|
|
124
|
+
const home = tempHome(t);
|
|
125
|
+
const status = await collectModelsStatus({
|
|
126
|
+
home,
|
|
127
|
+
env: { CLOUDFLARE_API_TOKEN: 'secret-cf', ACCOUNT_ID: '44444444444444444444444444444444', OLLAMA_BASE_URL: 'http://127.0.0.1:11434' },
|
|
128
|
+
fetchImpl: async () => response({ models: [] }),
|
|
129
|
+
providerFetchImpl: async (url, options) => {
|
|
130
|
+
seen.push({ url, auth: options.headers.authorization });
|
|
131
|
+
return response({ success: true, result: [
|
|
132
|
+
{ name: '@cf/qwen/code', task: { name: 'Text Generation' }, description: 'coding' },
|
|
133
|
+
{ name: '@cf/baai/embed', task: { name: 'Text Embeddings' }, description: 'embeddings' },
|
|
134
|
+
] });
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
assert.equal(seen.length, 1);
|
|
138
|
+
assert.match(seen[0].url, /accounts\/44444444444444444444444444444444\/ai\/models\/search$/);
|
|
139
|
+
assert.equal(status.discovery.cloudflare.ok, true);
|
|
140
|
+
assert.equal(status.discovery.cloudflare.returnedModelCount, 1);
|
|
141
|
+
assert.deepEqual(status.providerModels.cloudflare.map((row) => row.provider_model_id), ['@cf/qwen/code']);
|
|
142
|
+
assert.equal(status.providerModels.cloudflare[0].availability_source, 'provider_api');
|
|
143
|
+
assert.equal(status.providerModels.cloudflare[0].context_window_source, 'unknown');
|
|
144
|
+
const rendered = renderModelsStatus(status);
|
|
145
|
+
assert.match(rendered, /@cf\/qwen\/code/);
|
|
146
|
+
assert.doesNotMatch(rendered, /@cf\/baai\/embed/);
|
|
147
|
+
assert.doesNotMatch(rendered, /secret-cf/);
|
|
51
148
|
});
|
package/test/ollama.test.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
ollamaInstallPlan,
|
|
9
9
|
parseOllamaArgs,
|
|
10
10
|
probeOllama,
|
|
11
|
+
probeOllamaModel,
|
|
11
12
|
resolveOllamaConfig,
|
|
12
13
|
updateProjectOllamaConfig,
|
|
13
14
|
upsertOllamaEnvFile,
|
|
@@ -82,6 +83,26 @@ test('status probe recognizes configured chat and embedding models', async () =>
|
|
|
82
83
|
assert.equal(status.embed_ready, true);
|
|
83
84
|
});
|
|
84
85
|
|
|
86
|
+
test('local model metadata is accepted only after Ollama itself verifies the model', async () => {
|
|
87
|
+
let seenBody = null;
|
|
88
|
+
const fakeFetch = async (url, init) => {
|
|
89
|
+
assert.match(url, /\/api\/show$/);
|
|
90
|
+
seenBody = JSON.parse(init.body);
|
|
91
|
+
return new Response(JSON.stringify({
|
|
92
|
+
model_info: { 'qwen2.context_length': 32768 },
|
|
93
|
+
capabilities: ['completion', 'tools'],
|
|
94
|
+
details: { family: 'qwen2' },
|
|
95
|
+
}), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const result = await probeOllamaModel('qwen2.5-coder', OLLAMA_DEFAULTS, fakeFetch);
|
|
99
|
+
assert.equal(seenBody.model, 'qwen2.5-coder');
|
|
100
|
+
assert.equal(result.ok, true);
|
|
101
|
+
assert.equal(result.context_window, 32768);
|
|
102
|
+
assert.equal(result.context_window_source, 'local_runtime');
|
|
103
|
+
assert.deepEqual(result.capabilities, ['completion', 'tools']);
|
|
104
|
+
});
|
|
105
|
+
|
|
85
106
|
test('automatic install plans use local package managers, never a remote shell script', () => {
|
|
86
107
|
assert.deepEqual(ollamaInstallPlan('darwin', { brew: true }), {
|
|
87
108
|
command: 'brew', args: ['install', 'ollama'], manager: 'homebrew',
|
|
@@ -3,7 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import test from 'node:test';
|
|
6
|
-
import { describeProviderCredential, resolveProviderCredential } from '../src/lib/provider-credentials.js';
|
|
6
|
+
import { describeProviderCredential, ensureProviderEnvProfile, resolveProviderCredential } from '../src/lib/provider-credentials.js';
|
|
7
7
|
|
|
8
8
|
function fixtureHome(t) {
|
|
9
9
|
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-credentials-'));
|
|
@@ -50,3 +50,47 @@ test('AgentSam refuses provider credential files with broad POSIX permissions',
|
|
|
50
50
|
assert.equal(resolved.error, 'permissions_too_open');
|
|
51
51
|
assert.equal(resolved.value, '');
|
|
52
52
|
});
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
test('provider env profiles create a secure reusable source loader without embedding secrets', { skip: process.platform === 'win32' }, t => {
|
|
56
|
+
const home = fixtureHome(t);
|
|
57
|
+
const openai = ensureProviderEnvProfile('openai', { home });
|
|
58
|
+
assert.equal(openai.source_command, 'source ~/.agentsam/load-agent-env.sh openai');
|
|
59
|
+
assert.equal(fs.statSync(openai.file).mode & 0o777, 0o600);
|
|
60
|
+
assert.equal(fs.statSync(openai.loader).mode & 0o777, 0o700);
|
|
61
|
+
assert.match(fs.readFileSync(openai.file, 'utf8'), /OPENAI_API_KEY=""/);
|
|
62
|
+
assert.doesNotMatch(fs.readFileSync(openai.loader, 'utf8'), /sk-|secret-/);
|
|
63
|
+
|
|
64
|
+
const cloudflare = ensureProviderEnvProfile('cloudflare', { home });
|
|
65
|
+
const source = fs.readFileSync(cloudflare.file, 'utf8');
|
|
66
|
+
assert.match(source, /ACCOUNT_ID=""/);
|
|
67
|
+
assert.match(source, /CLOUDFLARE_API_TOKEN=""/);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('Cloudflare credential status carries non-secret account identity from the provider profile', t => {
|
|
71
|
+
const home = fixtureHome(t);
|
|
72
|
+
const profile = path.join(home, '.agentsam', 'env.d', 'cloudflare.env');
|
|
73
|
+
fs.writeFileSync(profile, 'export ACCOUNT_ID="0123456789abcdef0123456789abcdef"\nexport CLOUDFLARE_API_TOKEN="token-secret"\n', { mode: 0o600 });
|
|
74
|
+
if (process.platform !== 'win32') fs.chmodSync(profile, 0o600);
|
|
75
|
+
const resolved = resolveProviderCredential('cloudflare', { env: {}, home });
|
|
76
|
+
assert.equal(resolved.account_id, '0123456789abcdef0123456789abcdef');
|
|
77
|
+
const safe = describeProviderCredential('cloudflare', { env: {}, home });
|
|
78
|
+
assert.equal(safe.account_id, '0123456789abcdef0123456789abcdef');
|
|
79
|
+
assert.doesNotMatch(JSON.stringify(safe), /token-secret/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
test('Cloudflare profile account backfill never overwrites an existing token or account choice', t => {
|
|
84
|
+
const home = fixtureHome(t);
|
|
85
|
+
const profile = path.join(home, '.agentsam', 'env.d', 'cloudflare.env');
|
|
86
|
+
fs.writeFileSync(profile, 'export ACCOUNT_ID=""\nexport CLOUDFLARE_API_TOKEN="keep-me"\n', { mode: 0o600 });
|
|
87
|
+
ensureProviderEnvProfile('cloudflare', { home, accountId: '11111111111111111111111111111111' });
|
|
88
|
+
let source = fs.readFileSync(profile, 'utf8');
|
|
89
|
+
assert.match(source, /ACCOUNT_ID="11111111111111111111111111111111"/);
|
|
90
|
+
assert.match(source, /CLOUDFLARE_API_TOKEN="keep-me"/);
|
|
91
|
+
|
|
92
|
+
ensureProviderEnvProfile('cloudflare', { home, accountId: '22222222222222222222222222222222' });
|
|
93
|
+
source = fs.readFileSync(profile, 'utf8');
|
|
94
|
+
assert.match(source, /ACCOUNT_ID="11111111111111111111111111111111"/);
|
|
95
|
+
assert.doesNotMatch(source, /22222222222222222222222222222222/);
|
|
96
|
+
});
|
|
@@ -6,15 +6,23 @@ 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('
|
|
9
|
+
test('release candidate 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
|
+
const escapedVersion = pkg.version.replaceAll('.', '\\.');
|
|
13
14
|
|
|
14
|
-
assert.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
assert.match(
|
|
16
|
+
manifest,
|
|
17
|
+
new RegExp(`packages:[\\s\\S]*?root_sdk:[\\s\\S]*?version: "${escapedVersion}"`),
|
|
18
|
+
);
|
|
19
|
+
assert.match(
|
|
20
|
+
manifest,
|
|
21
|
+
new RegExp(`release:[\\s\\S]*?root_sdk:[\\s\\S]*?target_version: "${escapedVersion}"`),
|
|
22
|
+
);
|
|
23
|
+
assert.match(manifest, /state: (candidate|published)/);
|
|
24
|
+
assert.match(manifest, /current_latest: "\d+\.\d+\.\d+"/);
|
|
25
|
+
assert.match(manifest, /verification_command: npm run verify:release/);
|
|
18
26
|
assert.doesNotMatch(manifest, /^\s*- tui\s*$/m);
|
|
19
27
|
assert.doesNotMatch(readme, /agentsam tui|CLI\/TUI/);
|
|
20
28
|
});
|
|
@@ -13,7 +13,7 @@ function usage(input = 10_000, cumulative = input) {
|
|
|
13
13
|
estimate_kind: 'provider', provider_authoritative: true,
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
|
-
function cost(total = 0.1) { return { total_usd: total }; }
|
|
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
17
|
|
|
18
18
|
test('capability adapter hydrates packaged JSON schemas and tool surface exposes only selected executable schemas', () => {
|
|
19
19
|
const adapter = createCapabilityAdapter();
|
|
@@ -73,6 +73,8 @@ test('runner owns cwd, executes selected tool, preserves call_id and returns pro
|
|
|
73
73
|
assert.equal(result.output_text, 'done');
|
|
74
74
|
assert.equal(result.response_id, 'resp_2');
|
|
75
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);
|
|
76
78
|
assert.equal(result.continuation.compact_before_next_turn, false);
|
|
77
79
|
assert.ok(events.some(event => event.type === 'tool.search'));
|
|
78
80
|
assert.ok(events.some(event => event.type === 'tool.completed'));
|
package/test/shell.test.mjs
CHANGED
|
@@ -22,12 +22,15 @@ test('interactive prompt derives username and cwd instead of hardcoding Agent Sa
|
|
|
22
22
|
assert.equal(renderShellPrompt('/tmp/demo', env), 'alice /tmp/demo > ');
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
test('shell
|
|
25
|
+
test('shell startup stays quiet and points to the picker', () => {
|
|
26
26
|
const catalog = renderShellCatalog();
|
|
27
|
-
for (const command of ['/model', '/
|
|
27
|
+
for (const command of ['/model', '/usage', '/help', '/exit']) {
|
|
28
28
|
assert.match(catalog, new RegExp(command.replace('/', '\\/')));
|
|
29
29
|
}
|
|
30
|
-
assert.match(catalog, /
|
|
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/);
|
|
31
34
|
});
|
|
32
35
|
|
|
33
36
|
test('dispatch handles help, menu fallback, pwd, cd, and exit without falling through to host shell', async () => {
|
|
@@ -38,10 +41,11 @@ test('dispatch handles help, menu fallback, pwd, cd, and exit without falling th
|
|
|
38
41
|
const state = { cwd: root, write: (text) => { output += text; }, interactive: false };
|
|
39
42
|
let result = await dispatchShellLine('/help', state);
|
|
40
43
|
assert.equal(result.handled, true);
|
|
41
|
-
assert.match(output, /
|
|
44
|
+
assert.match(output, /Type normally to work with Agent Sam/);
|
|
45
|
+
assert.match(output, /agentsam help <topic>/);
|
|
42
46
|
output = '';
|
|
43
47
|
await dispatchShellLine('/', state);
|
|
44
|
-
assert.match(output, /
|
|
48
|
+
assert.match(output, /command picker/);
|
|
45
49
|
output = '';
|
|
46
50
|
await dispatchShellLine('/pwd', state);
|
|
47
51
|
assert.equal(output.trim(), root);
|
|
@@ -53,6 +57,44 @@ test('dispatch handles help, menu fallback, pwd, cd, and exit without falling th
|
|
|
53
57
|
assert.equal(result.exit, true);
|
|
54
58
|
});
|
|
55
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
|
+
|
|
56
98
|
test('reasoning and service-tier commands persist only supported controls for an exact model', async () => {
|
|
57
99
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-shell-model-'));
|
|
58
100
|
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'model-demo' }));
|
|
@@ -88,7 +130,7 @@ test('bare /context shows truthful economics without inventing active token usag
|
|
|
88
130
|
test('CLI supports a deterministic one-shot slash command for regression tests', () => {
|
|
89
131
|
const result = spawnSync(process.execPath, ['src/cli.js', 'shell', '--command', '/help'], { cwd: repoRoot, encoding: 'utf8' });
|
|
90
132
|
assert.equal(result.status, 0, result.stderr);
|
|
91
|
-
assert.match(result.stdout, /Agent Sam
|
|
92
|
-
assert.match(result.stdout,
|
|
93
|
-
assert.match(result.stdout,
|
|
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/);
|
|
94
136
|
});
|