@inneranimalmedia/agentsam-sdk 2.6.3 → 2.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTSAM.md +6 -0
- package/README.md +16 -1
- package/bin/agentsam +16 -1
- package/docs/BRAND_INTELLIGENCE.md +1 -1
- package/docs/architecture/AGENTSAM_DISTRIBUTION_OWNERSHIP.md +176 -0
- package/docs/architecture/AGENTSAM_GO_RUNTIME.md +282 -0
- package/docs/architecture/CODEBASEINDEX_GUIDED_PATH.md +202 -0
- package/docs/architecture/FS_E2E_CLOSURE_RECEIPT.md +59 -0
- package/docs/architecture/LOCAL_FS_AUTHORITY.md +38 -0
- package/docs/architecture/LOCAL_STUDIO_CLOUDFLARE_OAUTH.md +28 -0
- package/docs/architecture/PLAN_CLI_AND_LOCAL_STUDIO_DESKTOP.md +96 -0
- package/docs/architecture/SAM_ACTIVITY_RECOVERY_RECEIPT.md +39 -0
- package/docs/architecture/SAM_DECISION_WORK_RECEIPT.md +54 -0
- package/docs/architecture/SAM_KERNEL.md +181 -0
- package/docs/architecture/SAM_MACHINE_NORMALIZATION_PRECOMMIT_REPORT.md +208 -0
- package/docs/architecture/SLASH_SKILLS_PORTABLE.md +116 -0
- package/docs/architecture/fs-e2e-receipt.latest.json +42 -0
- package/docs/architecture/previews/codebaseindex-guided-path/CODEBASEINDEX_GUIDED_PATH.md +202 -0
- package/docs/architecture/previews/codebaseindex-guided-path/index.html +321 -0
- package/migrations/d1/0010_portable_tickets_memory.sql +140 -0
- package/migrations/d1/0011_agentsam_skill_v2.sql +185 -0
- package/migrations/d1/0011b_agentsam_skill_v2_cutover.sql +22 -0
- package/migrations/d1/0011c_agentsam_skill_v2_backfill.sql +76 -0
- package/migrations/d1/0011d_agentsam_skill_v2_retrieval_revisions.sql +50 -0
- package/migrations/d1/0012_agentsam_tools_required_seed.sql +67 -0
- package/migrations/d1/0013_identity_oauth_states.sql +15 -0
- package/migrations/d1/0014_auth_event_log.sql +20 -0
- package/migrations/d1/0015_identity_oauth_state_app_id.sql +3 -0
- package/migrations/d1/README_PORTABLE_CONTROL_PLANE.md +16 -0
- package/migrations/sqlite/agentsam_skill_retrieval.portable.sql +42 -0
- package/package.json +19 -4
- package/packages/agentsam-contracts/src/errors.ts +16 -0
- package/packages/agentsam-errors/src/envelope.js +53 -0
- package/packages/agentsam-errors/src/index.js +1 -0
- package/packages/agentsam-errors/src/recovery.js +301 -0
- package/packages/agentsam-knowledge/src/providers/index.js +18 -6
- package/packages/connectors/cloudflare/src/routes.js +9 -0
- package/packages/connectors/cloudflare/tests/connector.test.mjs +26 -1
- package/packages/identity/.agentsam/features/oauth-login-portal/agentsam.feature.json +1 -1
- package/packages/identity/.agentsam/features/oauth-login-portal/routes.json +11 -2
- package/packages/identity/docs/PORTABLE_IDENTITY_ARCHITECTURE.md +50 -0
- package/packages/identity/migrations/D1_SCHEMA_MAPPING.md +31 -0
- package/packages/identity/migrations/sqlite/001_identity_core.sql +99 -0
- package/packages/identity/migrations/sqlite/002_identity_oauth_client.sql +38 -0
- package/packages/identity/migrations/sqlite/003_identity_oauth_server.sql +64 -0
- package/packages/identity/package.json +2 -2
- package/packages/identity/src/adapters/cloudflare-d1/index.js +122 -22
- package/packages/identity/src/adapters/sqlite/index.js +319 -0
- package/packages/identity/src/app/verify-app.js +95 -0
- package/packages/identity/src/contracts/identity-store.js +115 -0
- package/packages/identity/src/contracts/route-ids.js +30 -0
- package/packages/identity/src/contracts/route-projection.js +218 -0
- package/packages/identity/src/contracts/routes.js +11 -0
- package/packages/identity/src/core/browser-paths.js +4 -5
- package/packages/identity/src/core/constants.js +13 -8
- package/packages/identity/src/core/session-policy.js +32 -0
- package/packages/identity/src/frontend/auth-portal/pages/login.html +10 -10
- package/packages/identity/src/frontend/auth-portal/pages/reset.html +3 -3
- package/packages/identity/src/frontend/auth-portal/pages/signup.html +3 -3
- package/packages/identity/src/frontend/auth-portal/preview/dashboard-stub.html +1 -1
- package/packages/identity/src/index.js +25 -0
- package/packages/identity/src/oauth/README.md +10 -4
- package/packages/identity/src/oauth/credentials.js +20 -13
- package/packages/identity/src/oauth/finalize-inbound.js +1 -1
- package/packages/identity/src/oauth/iam-platform.js +8 -7
- package/packages/identity/src/oauth/redirect-paths.js +27 -36
- package/packages/identity/src/server/identity-service.js +30 -13
- package/packages/identity/src/server/mount-policy.js +30 -0
- package/packages/identity/src/server/post-auth.js +79 -0
- package/packages/identity/src/server/worker-router.js +104 -72
- package/packages/identity/tests/finalize-inbound-oauth.test.mjs +6 -6
- package/packages/identity/tests/iam-provider.test.mjs +1 -1
- package/packages/identity/tests/identity-service.test.mjs +36 -5
- package/packages/identity/tests/oauth-credentials.test.mjs +3 -1
- package/packages/identity/tests/portable-identity-architecture.test.mjs +157 -0
- package/packages/identity/tests/session-routes-policy.test.mjs +21 -0
- package/packages/theme-church-site/package.json +2 -1
- package/packages/theme-church-site/src/index.js +1 -0
- package/packages/theme-companions-site/package.json +2 -1
- package/packages/theme-companions-site/src/index.js +1 -0
- package/packages/theme-floors-site/package.json +2 -1
- package/packages/theme-floors-site/src/index.js +1 -0
- package/packages/theme-fuelnfree-site/package.json +2 -1
- package/packages/theme-fuelnfree-site/src/index.js +1 -0
- package/packages/theme-handyman-site/package.json +2 -1
- package/packages/theme-handyman-site/src/index.js +1 -0
- package/packages/theme-insurance-site/package.json +2 -1
- package/packages/theme-insurance-site/src/index.js +1 -0
- package/packages/theme-shinshu-site/package.json +2 -1
- package/packages/theme-shinshu-site/src/index.js +1 -0
- package/protocol/apps/agentsam.app.v1.schema.json +51 -0
- package/protocol/brand/brandpack.v1.schema.json +43 -0
- package/protocol/credentials/issue.v1.schema.json +38 -0
- package/protocol/database/connection.v1.schema.json +41 -0
- package/protocol/embeddings/embedding-profile.v1.schema.json +20 -0
- package/protocol/errors/error-envelope.schema.json +135 -1
- package/protocol/errors/recovery.v1.schema.json +50 -0
- package/protocol/runtime/workspace-fs.v1.schema.json +71 -0
- package/protocol/sam/activity.v1.schema.json +48 -0
- package/protocol/sam/answer.v1.schema.json +35 -0
- package/protocol/sam/calibration.v1.schema.json +21 -0
- package/protocol/sam/decision-receipt.v1.schema.json +29 -0
- package/protocol/sam/evaluation.v1.schema.json +19 -0
- package/protocol/sam/operation.schema.json +66 -0
- package/protocol/sam/outcome.v1.schema.json +36 -0
- package/protocol/sam/question.v1.schema.json +26 -0
- package/protocol/sam/registry.seed.json +153 -0
- package/protocol/sam/result.schema.json +53 -0
- package/protocol/sam/state.v1.schema.json +23 -0
- package/protocol/skills/agentsam.interaction.v1.schema.json +50 -0
- package/protocol/skills/agentsam.skill.v1.schema.json +57 -0
- package/protocol/ui/icon-registry.mjs +226 -0
- package/protocol/ui/icon.v1.schema.json +51 -0
- package/skills/README.md +22 -9
- package/skills/agentsam-codebaseindex/SKILL.md +225 -0
- package/skills/catalog.json +14 -0
- package/src/cli/command-catalog.js +130 -0
- package/src/cli/dispatch.js +48 -0
- package/src/cli.js +43 -1
- package/src/commands/api-key.js +244 -0
- package/src/commands/app.js +60 -28
- package/src/commands/brand.js +17 -19
- package/src/commands/codebaseindex.js +688 -0
- package/src/commands/env.js +152 -25
- package/src/commands/go.js +366 -53
- package/src/commands/interaction-clack.js +115 -0
- package/src/commands/models.js +1 -1
- package/src/commands/providers.js +62 -14
- package/src/commands/shell.js +43 -2
- package/src/commands/skill.js +248 -0
- package/src/commands/skills.js +1 -1
- package/src/commands/start-local.js +4 -0
- package/src/commands/whoami.js +90 -18
- package/src/go/build.js +229 -39
- package/src/go/cloudflare.js +506 -105
- package/src/go/container.js +120 -0
- package/src/go/contract.js +9 -4
- package/src/go/discover.js +149 -33
- package/src/go/index.js +15 -3
- package/src/go/native-probe-runner.mjs +119 -0
- package/src/go/{registry.js → official-registry.js} +64 -7
- package/src/go/official-release.js +10 -0
- package/src/go/receipts.js +77 -10
- package/src/go/verify.js +9 -2
- package/src/index.js +27 -0
- package/src/indexing/ingest/discover-models.js +298 -0
- package/src/indexing/ingest/inventory.js +243 -0
- package/src/indexing/ingest/job-graph.js +181 -0
- package/src/indexing/ingest/materials.js +210 -0
- package/src/lib/provider-credentials.js +63 -21
- package/src/lib/slash-commands.js +1 -0
- package/src/local-fs/capability.js +121 -0
- package/src/local-fs/freshness.js +75 -0
- package/src/local-fs/index.js +385 -0
- package/src/local-fs/paths.js +100 -0
- package/src/local-pty/server.js +295 -19
- package/src/mcp/client.js +2 -2
- package/src/models/ai-access-onboarding.js +112 -0
- package/src/models/discovery.js +10 -2
- package/src/models/inventory-core.js +9 -1
- package/src/sam/activity/index.js +183 -0
- package/src/sam/client.js +252 -0
- package/src/sam/decision/calibration.js +109 -0
- package/src/sam/decision/confidence.js +126 -0
- package/src/sam/decision/evaluate.js +157 -0
- package/src/sam/decision/evaluators/deterministic.js +341 -0
- package/src/sam/decision/evaluators/heuristic.js +61 -0
- package/src/sam/decision/evaluators/select.js +50 -0
- package/src/sam/decision/evaluators/semantic.js +149 -0
- package/src/sam/decision/hierarchical.js +61 -0
- package/src/sam/decision/index.js +53 -0
- package/src/sam/decision/policy.js +86 -0
- package/src/sam/decision/questions.js +120 -0
- package/src/sam/decision/receipt.js +148 -0
- package/src/sam/decision/state.js +117 -0
- package/src/sam/decision/types.js +22 -0
- package/src/sam/decision/validate.js +200 -0
- package/src/sam/define.js +51 -0
- package/src/sam/index.js +65 -0
- package/src/sam/operations/brand-scan.js +62 -0
- package/src/sam/operations/cad-blender-inspect.js +36 -0
- package/src/sam/operations/codebaseindex-ingest.js +49 -0
- package/src/sam/operations/decision-evaluate.js +59 -0
- package/src/sam/operations/planning-astar.js +77 -0
- package/src/sam/operations/planning-goap.js +60 -0
- package/src/sam/operations/repository-inspect.js +72 -0
- package/src/sam/operations/security-scan.js +33 -0
- package/src/sam/operations/terminal-exec.js +29 -0
- package/src/sam/planning/astar.js +311 -0
- package/src/sam/planning/goap.js +177 -0
- package/src/sam/planning/index.js +21 -0
- package/src/sam/planning/state.js +84 -0
- package/src/sam/registry.js +48 -0
- package/src/sam/result.js +77 -0
- package/src/sam/seed.js +44 -0
- package/src/sam/types.js +91 -0
- package/src/skills/catalog.js +64 -0
- package/src/skills/content-resolver.js +124 -0
- package/src/skills/hosted-store.js +37 -0
- package/src/skills/index.js +29 -64
- package/src/skills/interaction.js +102 -0
- package/src/skills/local-store.js +228 -0
- package/src/skills/manifest.js +104 -0
- package/src/skills/metrics.js +31 -0
- package/src/skills/registry.js +184 -0
- package/src/skills/runtime.js +287 -0
- package/src/skills/slash.js +44 -0
- package/src/ui/cli/help.js +94 -101
- package/test/cli/api-key-env-whoami.test.mjs +129 -0
- package/test/cli/codebaseindex-plan-ux.test.mjs +37 -0
- package/test/cli/go.test.mjs +62 -5
- package/test/cli/skill-npm-and-env.test.mjs +52 -0
- package/test/cli/wireframes-go-registry.test.mjs +87 -2
- package/test/go/build-source-identity.test.mjs +31 -0
- package/test/go/cloudflare-probe.test.mjs +274 -10
- package/test/go/distribution.test.mjs +28 -0
- package/test/integration/ai-access-onboarding.test.mjs +47 -0
- package/test/integration/cli-help.test.mjs +1 -1
- package/test/integration/cms-site-tenancy-contract.test.mjs +6 -6
- package/test/integration/codebaseindex-ingest.test.mjs +166 -0
- package/test/integration/icon-registry.test.mjs +67 -0
- package/test/integration/ingest-discover-models.test.mjs +30 -0
- package/test/integration/install-script.test.mjs +12 -9
- package/test/integration/local-fs.test.mjs +113 -0
- package/test/integration/provider-env-cli.test.mjs +2 -1
- package/test/integration/sam-activity-recovery.test.mjs +147 -0
- package/test/integration/sam-decision.test.mjs +584 -0
- package/test/integration/sam-kernel.test.mjs +99 -0
- package/test/integration/sam-planning-astar.test.mjs +279 -0
- package/test/integration/skill-runtime.test.mjs +240 -0
- package/test/integration/studio-fs-pty-e2e.test.mjs +294 -0
- package/test/models.test.mjs +14 -7
- package/test/shell.test.mjs +4 -4
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import {
|
|
4
|
+
planAStar,
|
|
5
|
+
planGoap,
|
|
6
|
+
ASTAR_ENGINE,
|
|
7
|
+
GOAP_ENGINE,
|
|
8
|
+
stableStateKey,
|
|
9
|
+
} from '../../src/sam/planning/index.js';
|
|
10
|
+
import {
|
|
11
|
+
brandGoapActions,
|
|
12
|
+
brandWorldFromPlan,
|
|
13
|
+
brandGoalFromPlan,
|
|
14
|
+
} from '../../packages/agentsam-brand/src/index.js';
|
|
15
|
+
|
|
16
|
+
describe('SAM A* planner', () => {
|
|
17
|
+
it('finds cheapest multi-step path over expensive direct edge', () => {
|
|
18
|
+
// Graph: start --1--> a --1--> goal (cost 2)
|
|
19
|
+
// start --10--> goal (cost 10)
|
|
20
|
+
// Greedy-on-first-step with wrong heuristic could still pick expensive if we only looked local;
|
|
21
|
+
// A* with h=0 must pick the cheap two-step path.
|
|
22
|
+
const edges = {
|
|
23
|
+
start: [
|
|
24
|
+
{ action: 'via_a', nextState: 'a', cost: 1 },
|
|
25
|
+
{ action: 'direct', nextState: 'goal', cost: 10 },
|
|
26
|
+
],
|
|
27
|
+
a: [{ action: 'finish', nextState: 'goal', cost: 1 }],
|
|
28
|
+
goal: [],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const result = planAStar({
|
|
32
|
+
initialState: 'start',
|
|
33
|
+
isGoal: (s) => s === 'goal',
|
|
34
|
+
expand: (s) => edges[s] || [],
|
|
35
|
+
stateKey: (s) => String(s),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
assert.equal(result.status, 'found');
|
|
39
|
+
assert.equal(result.ok, true);
|
|
40
|
+
assert.equal(result.engine, ASTAR_ENGINE);
|
|
41
|
+
assert.equal(result.total_cost, 2);
|
|
42
|
+
assert.deepEqual(result.plan.map((p) => p.action), ['via_a', 'finish']);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('does not get fooled by a cheap first action leading to an expensive future', () => {
|
|
46
|
+
// start --1--> trap --100--> goal
|
|
47
|
+
// start --5--> good --1--> goal
|
|
48
|
+
// Greedy "cheapest applicable" would take trap first (cost 1).
|
|
49
|
+
// Optimal is good path (cost 6).
|
|
50
|
+
const edges = {
|
|
51
|
+
start: [
|
|
52
|
+
{ action: 'trap', nextState: 'trap', cost: 1 },
|
|
53
|
+
{ action: 'good', nextState: 'good', cost: 5 },
|
|
54
|
+
],
|
|
55
|
+
trap: [{ action: 'escape', nextState: 'goal', cost: 100 }],
|
|
56
|
+
good: [{ action: 'arrive', nextState: 'goal', cost: 1 }],
|
|
57
|
+
goal: [],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const greedy = (() => {
|
|
61
|
+
const sequence = [];
|
|
62
|
+
let state = 'start';
|
|
63
|
+
for (let i = 0; i < 10; i += 1) {
|
|
64
|
+
if (state === 'goal') break;
|
|
65
|
+
const candidates = (edges[state] || []).slice().sort((a, b) => a.cost - b.cost);
|
|
66
|
+
if (!candidates.length) break;
|
|
67
|
+
sequence.push(candidates[0].action);
|
|
68
|
+
state = candidates[0].nextState;
|
|
69
|
+
}
|
|
70
|
+
return sequence;
|
|
71
|
+
})();
|
|
72
|
+
assert.deepEqual(greedy, ['trap', 'escape'], 'sanity: greedy would pick trap');
|
|
73
|
+
|
|
74
|
+
const result = planAStar({
|
|
75
|
+
initialState: 'start',
|
|
76
|
+
isGoal: (s) => s === 'goal',
|
|
77
|
+
expand: (s) => edges[s] || [],
|
|
78
|
+
stateKey: (s) => String(s),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
assert.equal(result.status, 'found');
|
|
82
|
+
assert.equal(result.total_cost, 6);
|
|
83
|
+
assert.deepEqual(result.plan.map((p) => p.action), ['good', 'arrive']);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('terminates with cycles without looping forever', () => {
|
|
87
|
+
const edges = {
|
|
88
|
+
a: [
|
|
89
|
+
{ action: 'loop', nextState: 'a', cost: 1 },
|
|
90
|
+
{ action: 'to_b', nextState: 'b', cost: 2 },
|
|
91
|
+
],
|
|
92
|
+
b: [{ action: 'to_goal', nextState: 'goal', cost: 1 }],
|
|
93
|
+
goal: [],
|
|
94
|
+
};
|
|
95
|
+
const result = planAStar({
|
|
96
|
+
initialState: 'a',
|
|
97
|
+
isGoal: (s) => s === 'goal',
|
|
98
|
+
expand: (s) => edges[s] || [],
|
|
99
|
+
stateKey: (s) => String(s),
|
|
100
|
+
maxExpansions: 100,
|
|
101
|
+
});
|
|
102
|
+
assert.equal(result.status, 'found');
|
|
103
|
+
assert.ok(result.expanded_states < 50);
|
|
104
|
+
assert.deepEqual(result.plan.map((p) => p.action), ['to_b', 'to_goal']);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('replaces worse paths when a better g-score is found', () => {
|
|
108
|
+
// start --5--> mid --1--> goal (total 6, discovered first via mid)
|
|
109
|
+
// start --1--> side --1--> mid (reaches mid with g=2, should replace)
|
|
110
|
+
const edges = {
|
|
111
|
+
start: [
|
|
112
|
+
{ action: 'long_to_mid', nextState: 'mid', cost: 5 },
|
|
113
|
+
{ action: 'to_side', nextState: 'side', cost: 1 },
|
|
114
|
+
],
|
|
115
|
+
side: [{ action: 'side_to_mid', nextState: 'mid', cost: 1 }],
|
|
116
|
+
mid: [{ action: 'mid_to_goal', nextState: 'goal', cost: 1 }],
|
|
117
|
+
goal: [],
|
|
118
|
+
};
|
|
119
|
+
const result = planAStar({
|
|
120
|
+
initialState: 'start',
|
|
121
|
+
isGoal: (s) => s === 'goal',
|
|
122
|
+
expand: (s) => edges[s] || [],
|
|
123
|
+
stateKey: (s) => String(s),
|
|
124
|
+
});
|
|
125
|
+
assert.equal(result.status, 'found');
|
|
126
|
+
assert.equal(result.total_cost, 3);
|
|
127
|
+
assert.deepEqual(result.plan.map((p) => p.action), ['to_side', 'side_to_mid', 'mid_to_goal']);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('returns no_plan for unsatisfiable goals', () => {
|
|
131
|
+
const result = planAStar({
|
|
132
|
+
initialState: 'a',
|
|
133
|
+
isGoal: (s) => s === 'z',
|
|
134
|
+
expand: (s) => (s === 'a' ? [{ action: 'to_b', nextState: 'b', cost: 1 }] : []),
|
|
135
|
+
stateKey: (s) => String(s),
|
|
136
|
+
});
|
|
137
|
+
assert.equal(result.status, 'no_plan');
|
|
138
|
+
assert.equal(result.ok, false);
|
|
139
|
+
assert.deepEqual(result.plan, []);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('returns budget_exhausted when maxExpansions is hit', () => {
|
|
143
|
+
const result = planAStar({
|
|
144
|
+
initialState: 0,
|
|
145
|
+
isGoal: (s) => s === 1000,
|
|
146
|
+
expand: (s) => [{ action: 'inc', nextState: Number(s) + 1, cost: 1 }],
|
|
147
|
+
stateKey: (s) => String(s),
|
|
148
|
+
maxExpansions: 3,
|
|
149
|
+
});
|
|
150
|
+
assert.equal(result.status, 'budget_exhausted');
|
|
151
|
+
assert.equal(result.ok, false);
|
|
152
|
+
assert.equal(result.expanded_states, 3);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('returns cancelled when AbortSignal aborts', () => {
|
|
156
|
+
const controller = new AbortController();
|
|
157
|
+
controller.abort();
|
|
158
|
+
const result = planAStar({
|
|
159
|
+
initialState: 'a',
|
|
160
|
+
isGoal: () => false,
|
|
161
|
+
expand: () => [{ action: 'x', nextState: 'b', cost: 1 }],
|
|
162
|
+
signal: controller.signal,
|
|
163
|
+
});
|
|
164
|
+
assert.equal(result.status, 'cancelled');
|
|
165
|
+
assert.equal(result.ok, false);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('tie-breaks deterministically', () => {
|
|
169
|
+
// Two equal-cost paths to goal; order of expansion must be stable across runs.
|
|
170
|
+
const edges = {
|
|
171
|
+
start: [
|
|
172
|
+
{ action: 'b_first_listed', nextState: 'b', cost: 1 },
|
|
173
|
+
{ action: 'a_second_listed', nextState: 'a', cost: 1 },
|
|
174
|
+
],
|
|
175
|
+
a: [{ action: 'a_goal', nextState: 'goal', cost: 1 }],
|
|
176
|
+
b: [{ action: 'b_goal', nextState: 'goal', cost: 1 }],
|
|
177
|
+
goal: [],
|
|
178
|
+
};
|
|
179
|
+
const run = () => planAStar({
|
|
180
|
+
initialState: 'start',
|
|
181
|
+
isGoal: (s) => s === 'goal',
|
|
182
|
+
expand: (s) => edges[s] || [],
|
|
183
|
+
stateKey: (s) => String(s),
|
|
184
|
+
});
|
|
185
|
+
const first = run();
|
|
186
|
+
const second = run();
|
|
187
|
+
assert.equal(first.status, 'found');
|
|
188
|
+
assert.deepEqual(first.plan.map((p) => p.action), second.plan.map((p) => p.action));
|
|
189
|
+
assert.equal(first.total_cost, 2);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('does not mutate input states during search', () => {
|
|
193
|
+
const initial = { x: 1 };
|
|
194
|
+
const frozen = JSON.stringify(initial);
|
|
195
|
+
planAStar({
|
|
196
|
+
initialState: initial,
|
|
197
|
+
isGoal: (s) => s.x === 3,
|
|
198
|
+
expand: (s) => [{ action: 'bump', nextState: { x: s.x + 1 }, cost: 1 }],
|
|
199
|
+
stateKey: stableStateKey,
|
|
200
|
+
});
|
|
201
|
+
assert.equal(JSON.stringify(initial), frozen);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('SAM GOAP adapter', () => {
|
|
206
|
+
it('plans with preconditions and effects', () => {
|
|
207
|
+
const actions = [
|
|
208
|
+
{
|
|
209
|
+
id: 'gather_wood',
|
|
210
|
+
cost: 2,
|
|
211
|
+
preconditions: { has_wood: false },
|
|
212
|
+
effects: { has_wood: true },
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
id: 'build_fire',
|
|
216
|
+
cost: 3,
|
|
217
|
+
preconditions: { has_wood: true, fire: false },
|
|
218
|
+
effects: { fire: true },
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: 'magic_fire',
|
|
222
|
+
cost: 50,
|
|
223
|
+
preconditions: { fire: false },
|
|
224
|
+
effects: { fire: true, has_wood: true },
|
|
225
|
+
},
|
|
226
|
+
];
|
|
227
|
+
const result = planGoap({
|
|
228
|
+
initialState: { has_wood: false, fire: false },
|
|
229
|
+
goal: { fire: true },
|
|
230
|
+
actions,
|
|
231
|
+
});
|
|
232
|
+
assert.equal(result.status, 'found');
|
|
233
|
+
assert.equal(result.engine, GOAP_ENGINE);
|
|
234
|
+
assert.deepEqual(result.plan, ['gather_wood', 'build_fire']);
|
|
235
|
+
assert.equal(result.total_cost, 5);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('does not mutate action catalog or world during search', () => {
|
|
239
|
+
const actions = [
|
|
240
|
+
{
|
|
241
|
+
id: 'step',
|
|
242
|
+
cost: 1,
|
|
243
|
+
preconditions: { done: false },
|
|
244
|
+
effects: { done: true },
|
|
245
|
+
},
|
|
246
|
+
];
|
|
247
|
+
const world = { done: false };
|
|
248
|
+
const actionsSnap = JSON.stringify(actions);
|
|
249
|
+
const worldSnap = JSON.stringify(world);
|
|
250
|
+
planGoap({ initialState: world, goal: { done: true }, actions });
|
|
251
|
+
assert.equal(JSON.stringify(actions), actionsSnap);
|
|
252
|
+
assert.equal(JSON.stringify(world), worldSnap);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('brand GOAP actions plan through A* engine', () => {
|
|
256
|
+
const plan = {
|
|
257
|
+
current_state: {
|
|
258
|
+
brand_scan_complete: true,
|
|
259
|
+
canonical_color_tokens: false,
|
|
260
|
+
duplicate_color_count: 8,
|
|
261
|
+
canonical_typography: false,
|
|
262
|
+
shared_button_component: false,
|
|
263
|
+
header_consistency: false,
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
const result = planGoap({
|
|
267
|
+
initialState: brandWorldFromPlan(plan),
|
|
268
|
+
goal: brandGoalFromPlan(plan),
|
|
269
|
+
actions: brandGoapActions(),
|
|
270
|
+
});
|
|
271
|
+
assert.equal(result.engine, GOAP_ENGINE);
|
|
272
|
+
assert.equal(result.status, 'found');
|
|
273
|
+
assert.ok(result.plan.includes('establish-color-token-map'));
|
|
274
|
+
assert.ok(result.plan.includes('verify-brand-surfaces'));
|
|
275
|
+
assert.ok(result.plan.includes('establish-type-roles'));
|
|
276
|
+
assert.ok(result.total_cost > 0);
|
|
277
|
+
assert.equal(result.final_state.brand_verified, true);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
@@ -0,0 +1,240 @@
|
|
|
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 {
|
|
7
|
+
parseSlashInvocation,
|
|
8
|
+
canonicalizeSlashTrigger,
|
|
9
|
+
SkillRegistry,
|
|
10
|
+
SkillRuntime,
|
|
11
|
+
SkillContentResolver,
|
|
12
|
+
createFilesystemObjectStore,
|
|
13
|
+
createMemoryDatabaseContent,
|
|
14
|
+
createUserSkill,
|
|
15
|
+
installSkillFromPath,
|
|
16
|
+
aliasSkill,
|
|
17
|
+
computeSkillContentMetrics,
|
|
18
|
+
} from '../../src/skills/index.js';
|
|
19
|
+
|
|
20
|
+
function tmpHome() {
|
|
21
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-skill-'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
test('prose never parses as slash invocation', () => {
|
|
25
|
+
assert.equal(parseSlashInvocation('Hello /deploy please'), null);
|
|
26
|
+
assert.equal(parseSlashInvocation('deploy this'), null);
|
|
27
|
+
assert.equal(parseSlashInvocation(''), null);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('canonical slash parser lowercases and rejects underscores in match path via canonicalize', () => {
|
|
31
|
+
assert.deepEqual(parseSlashInvocation('/CodeBaseIndex .'), {
|
|
32
|
+
trigger: '/codebaseindex',
|
|
33
|
+
args: '.',
|
|
34
|
+
});
|
|
35
|
+
assert.deepEqual(parseSlashInvocation('/cad-export assembly.step'), {
|
|
36
|
+
trigger: '/cad-export',
|
|
37
|
+
args: 'assembly.step',
|
|
38
|
+
});
|
|
39
|
+
assert.equal(canonicalizeSlashTrigger('/Foo_Bar'), '/foo-bar');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('local user skill resolves with zero cloud resources', async () => {
|
|
43
|
+
const home = tmpHome();
|
|
44
|
+
const created = createUserSkill('release-check', { home });
|
|
45
|
+
fs.writeFileSync(
|
|
46
|
+
path.join(created.root, 'SKILL.md'),
|
|
47
|
+
'# Release check\n\nVerify preview then production.\n',
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const runtime = new SkillRuntime({
|
|
51
|
+
registry: new SkillRegistry({ home, builtins: false }),
|
|
52
|
+
contentResolver: new SkillContentResolver(),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const prose = await runtime.invoke({ input: 'please release check main' });
|
|
56
|
+
assert.equal(prose.matched, false);
|
|
57
|
+
|
|
58
|
+
const hit = await runtime.invoke({ input: '/release-check main' });
|
|
59
|
+
assert.equal(hit.matched, true);
|
|
60
|
+
assert.equal(hit.invocation.trigger, '/release-check');
|
|
61
|
+
assert.equal(hit.invocation.args, 'main');
|
|
62
|
+
assert.equal(hit.skill.id, 'release-check');
|
|
63
|
+
assert.equal(hit.interaction.status, 'ready');
|
|
64
|
+
assert.match(hit.modelInstructions.content, /Release check/);
|
|
65
|
+
assert.match(hit.receipt.checksum, /^sha256:/);
|
|
66
|
+
assert.equal(hit.receipt.contentSource, 'local_file');
|
|
67
|
+
assert.equal(hit.receipt.source, 'user');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('slash collision requires alias resolution', () => {
|
|
71
|
+
const home = tmpHome();
|
|
72
|
+
const dirA = path.join(home, 'pkg-a');
|
|
73
|
+
const dirB = path.join(home, 'pkg-b');
|
|
74
|
+
fs.mkdirSync(dirA, { recursive: true });
|
|
75
|
+
fs.mkdirSync(dirB, { recursive: true });
|
|
76
|
+
for (const [dir, id] of [
|
|
77
|
+
[dirA, 'deploy-a'],
|
|
78
|
+
[dirB, 'deploy-b'],
|
|
79
|
+
]) {
|
|
80
|
+
fs.writeFileSync(
|
|
81
|
+
path.join(dir, 'agentsam.skill.json'),
|
|
82
|
+
JSON.stringify({
|
|
83
|
+
schema: 'agentsam.skill.v1',
|
|
84
|
+
id,
|
|
85
|
+
name: id,
|
|
86
|
+
slash: { suggested: '/deploy' },
|
|
87
|
+
instructions: { source: 'inline', inline: `# ${id}` },
|
|
88
|
+
execution: { mode: 'turn', tools: [], operations: [] },
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
installSkillFromPath(dirA, { home });
|
|
94
|
+
assert.throws(() => installSkillFromPath(dirB, { home }), (err) => err.code === 'SLASH_COLLISION');
|
|
95
|
+
const second = installSkillFromPath(dirB, { home, aliasOnCollision: '/deploy-b' });
|
|
96
|
+
assert.equal(second.trigger, '/deploy-b');
|
|
97
|
+
|
|
98
|
+
const registry = new SkillRegistry({ home, builtins: false });
|
|
99
|
+
assert.equal(registry.getByTrigger('/deploy')?.id, 'deploy-a');
|
|
100
|
+
assert.equal(registry.getByTrigger('/deploy-b')?.id, 'deploy-b');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('unknown slash returns suggestions without model', async () => {
|
|
104
|
+
const home = tmpHome();
|
|
105
|
+
createUserSkill('deploy', { home });
|
|
106
|
+
createUserSkill('deploy-check', { home });
|
|
107
|
+
const runtime = new SkillRuntime({
|
|
108
|
+
registry: new SkillRegistry({ home, builtins: false }),
|
|
109
|
+
});
|
|
110
|
+
const miss = await runtime.invoke({ input: '/deply' });
|
|
111
|
+
assert.equal(miss.matched, true);
|
|
112
|
+
assert.equal(miss.interaction.status, 'blocked');
|
|
113
|
+
assert.equal(miss.interaction.prompt.code, 'SKILL_NOT_FOUND');
|
|
114
|
+
assert.match(miss.interaction.prompt.message, /Did you mean/);
|
|
115
|
+
assert.match(miss.interaction.prompt.message, /\/deploy/);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('package_file and object_store adapters resolve without R2 coupling', async () => {
|
|
119
|
+
const home = tmpHome();
|
|
120
|
+
const pkg = path.join(home, 'pkg');
|
|
121
|
+
fs.mkdirSync(pkg, { recursive: true });
|
|
122
|
+
fs.writeFileSync(path.join(pkg, 'SKILL.md'), '# From package\n');
|
|
123
|
+
fs.writeFileSync(
|
|
124
|
+
path.join(pkg, 'agentsam.skill.json'),
|
|
125
|
+
JSON.stringify({
|
|
126
|
+
schema: 'agentsam.skill.v1',
|
|
127
|
+
id: 'pkg-skill',
|
|
128
|
+
name: 'Pkg',
|
|
129
|
+
slash: { suggested: '/pkg-skill' },
|
|
130
|
+
instructions: { source: 'package_file', ref: './SKILL.md' },
|
|
131
|
+
execution: { mode: 'turn' },
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
installSkillFromPath(pkg, { home });
|
|
135
|
+
|
|
136
|
+
const storeRoot = path.join(home, 'objects');
|
|
137
|
+
fs.mkdirSync(storeRoot, { recursive: true });
|
|
138
|
+
fs.writeFileSync(path.join(storeRoot, 'remote.md'), '# From object store\n');
|
|
139
|
+
|
|
140
|
+
const resolver = new SkillContentResolver({
|
|
141
|
+
objectStore: createFilesystemObjectStore(storeRoot),
|
|
142
|
+
database: createMemoryDatabaseContent(new Map([['db:1', '# From database\n']])),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const runtime = new SkillRuntime({
|
|
146
|
+
registry: new SkillRegistry({ home, builtins: false }),
|
|
147
|
+
contentResolver: resolver,
|
|
148
|
+
});
|
|
149
|
+
const pkgHit = await runtime.invoke({ input: '/pkg-skill' });
|
|
150
|
+
assert.match(pkgHit.modelInstructions.content, /From package/);
|
|
151
|
+
|
|
152
|
+
const objectManifest = {
|
|
153
|
+
schema: 'agentsam.skill.v1',
|
|
154
|
+
id: 'obj-skill',
|
|
155
|
+
name: 'Obj',
|
|
156
|
+
instructions: { source: 'object_store', ref: 'remote.md' },
|
|
157
|
+
execution: { mode: 'turn' },
|
|
158
|
+
};
|
|
159
|
+
const obj = await resolver.resolve(objectManifest);
|
|
160
|
+
assert.match(obj.content, /object store/);
|
|
161
|
+
|
|
162
|
+
const db = await resolver.resolve({
|
|
163
|
+
schema: 'agentsam.skill.v1',
|
|
164
|
+
id: 'db-skill',
|
|
165
|
+
name: 'Db',
|
|
166
|
+
instructions: { source: 'database', ref: 'db:1' },
|
|
167
|
+
execution: { mode: 'turn' },
|
|
168
|
+
});
|
|
169
|
+
assert.match(db.content, /database/);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('tool requirements block without subagent lookup', async () => {
|
|
173
|
+
const home = tmpHome();
|
|
174
|
+
const dir = path.join(home, 'toolish');
|
|
175
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
176
|
+
fs.writeFileSync(
|
|
177
|
+
path.join(dir, 'agentsam.skill.json'),
|
|
178
|
+
JSON.stringify({
|
|
179
|
+
schema: 'agentsam.skill.v1',
|
|
180
|
+
id: 'needs-tools',
|
|
181
|
+
name: 'Needs tools',
|
|
182
|
+
slash: { suggested: '/needs-tools' },
|
|
183
|
+
instructions: { source: 'inline', inline: '# Need tools' },
|
|
184
|
+
execution: {
|
|
185
|
+
mode: 'turn',
|
|
186
|
+
tools: ['repository.inspect', 'missing.tool'],
|
|
187
|
+
operations: ['codebaseindex.ingest'],
|
|
188
|
+
},
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
installSkillFromPath(dir, { home });
|
|
192
|
+
|
|
193
|
+
const runtime = new SkillRuntime({
|
|
194
|
+
registry: new SkillRegistry({ home, builtins: false }),
|
|
195
|
+
toolRegistry: { has: (id) => id === 'repository.inspect' },
|
|
196
|
+
});
|
|
197
|
+
const result = await runtime.invoke({ input: '/needs-tools' });
|
|
198
|
+
assert.equal(result.interaction.status, 'blocked');
|
|
199
|
+
assert.equal(result.interaction.prompt.code, 'TOOL_REQUIRED');
|
|
200
|
+
assert.match(result.interaction.prompt.message, /missing\.tool/);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('alias remaps trigger; metrics from resolved content', () => {
|
|
204
|
+
const home = tmpHome();
|
|
205
|
+
createUserSkill('brand-scan', { home });
|
|
206
|
+
aliasSkill('brand-scan', '/brand', { home });
|
|
207
|
+
const registry = new SkillRegistry({ home, builtins: false });
|
|
208
|
+
assert.equal(registry.getByTrigger('/brand')?.id, 'brand-scan');
|
|
209
|
+
const metrics = computeSkillContentMetrics('# hi\n\nworld');
|
|
210
|
+
assert.ok(metrics.byte_size > 0);
|
|
211
|
+
assert.ok(metrics.estimated_tokens > 0);
|
|
212
|
+
assert.match(metrics.checksum, /^sha256:/);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('/skills lists vocabulary', async () => {
|
|
216
|
+
const home = tmpHome();
|
|
217
|
+
createUserSkill('one', { home });
|
|
218
|
+
const runtime = new SkillRuntime({
|
|
219
|
+
registry: new SkillRegistry({ home, builtins: false }),
|
|
220
|
+
});
|
|
221
|
+
const result = await runtime.invoke({ input: '/skills' });
|
|
222
|
+
assert.equal(result.matched, true);
|
|
223
|
+
assert.equal(result.interaction.status, 'complete');
|
|
224
|
+
assert.match(result.interaction.prompt.message, /\/one/);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('/skills empty registry offers create path not a circular list tip', async () => {
|
|
228
|
+
const home = tmpHome();
|
|
229
|
+
const runtime = new SkillRuntime({
|
|
230
|
+
registry: new SkillRegistry({ home, builtins: false }),
|
|
231
|
+
});
|
|
232
|
+
const empty = await runtime.invoke({ input: '/skills' });
|
|
233
|
+
assert.equal(empty.interaction.status, 'needs_input');
|
|
234
|
+
assert.equal(empty.interaction.prompt.code, 'NO_SKILLS');
|
|
235
|
+
assert.match(empty.interaction.prompt.message, /agentsam skill create/);
|
|
236
|
+
assert.doesNotMatch(empty.interaction.prompt.message, /agentsam skill list/);
|
|
237
|
+
|
|
238
|
+
const hinted = await runtime.invoke({ input: '/skills release-check' });
|
|
239
|
+
assert.match(hinted.interaction.prompt.message, /skill create release-check/);
|
|
240
|
+
});
|